{% extends "base.html" %}
{% block title %}sendword — {% if is_new %}new hook{% else %}edit {{ form_name }}{% endif %}{% endblock %}
{% block crumbs %}
<div class="wf-crumbs">
<a href="/">HOOKS</a><span class="sep">/</span>
{% if is_new %}
<span aria-current="page">NEW</span>
{% else %}
<a href="/hooks/{{ slug }}">{{ slug | upper }}</a><span class="sep">/</span>
<span aria-current="page">EDIT</span>
{% endif %}
</div>
{% endblock %}
{% block content %}
<form method="post" action="{% if is_new %}/hooks/new{% else %}/hooks/{{ slug }}/edit{% endif %}">
{# --- Basic Info --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">BASIC INFO</span></div>
<div class="wf-panel-body">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="name">Name</label>
<input type="text" id="name" name="name" value="{{ form_name }}" required placeholder="Deploy App" class="wf-input">
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="slug">Slug</label>
<input type="text" id="slug" name="slug" value="{{ form_slug }}" {% if not is_new %}readonly{% endif %} required pattern="[a-z0-9]+(-[a-z0-9]+)*" maxlength="64" placeholder="deploy-app" class="wf-input">
{% if is_new %}
<span class="wf-field-hint">Lowercase letters, numbers, and hyphens. Used in the webhook URL.</span>
{% else %}
<span class="wf-field-hint">Slug cannot be changed after creation.</span>
{% endif %}
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="description">Description</label>
<textarea id="description" name="description" rows="2" placeholder="Optional description" class="wf-textarea">{{ form_description }}</textarea>
</div>
<label class="wf-check-row">
<input type="checkbox" name="enabled" value="true" {% if form_enabled %}checked{% endif %} class="wf-switch">
<span>Enabled</span>
</label>
</div>
</div>
{# --- Authentication --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">AUTHENTICATION</span></div>
<div class="wf-panel-body">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="auth_mode">Auth mode</label>
<select id="auth_mode" name="auth_mode" class="wf-select" onchange="toggleAuthFields()">
<option value="none" {% if form_auth_mode == "none" %}selected{% endif %}>None (public)</option>
<option value="bearer" {% if form_auth_mode == "bearer" %}selected{% endif %}>Bearer token</option>
<option value="hmac" {% if form_auth_mode == "hmac" %}selected{% endif %}>HMAC signature</option>
</select>
<span class="wf-field-hint">How callers authenticate when triggering this hook.</span>
</div>
<div id="bearer-fields">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="auth_token">Bearer token</label>
<input type="text" id="auth_token" name="auth_token" value="{{ form_auth_token }}" placeholder="${HOOK_TOKEN} or literal value" class="wf-input">
<span class="wf-field-hint">Use ${ENV_VAR} syntax to reference an environment variable.</span>
</div>
</div>
<div id="hmac-fields">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="auth_header">Signature header</label>
<input type="text" id="auth_header" name="auth_header" value="{{ form_auth_header }}" placeholder="X-Hub-Signature-256" class="wf-input">
<span class="wf-field-hint">HTTP header containing the HMAC signature.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="auth_algorithm">Algorithm</label>
<select id="auth_algorithm" name="auth_algorithm" class="wf-select">
<option value="sha256" {% if form_auth_algorithm == "sha256" %}selected{% endif %}>SHA-256</option>
</select>
</div>
<div class="wf-field">
<label class="wf-label" for="auth_secret">Shared secret</label>
<input type="text" id="auth_secret" name="auth_secret" value="{{ form_auth_secret }}" placeholder="${WEBHOOK_SECRET} or literal value" class="wf-input">
<span class="wf-field-hint">Use ${ENV_VAR} syntax to reference an environment variable.</span>
</div>
</div>
</div>
</div>
{# --- Executor --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">EXECUTOR</span></div>
<div class="wf-panel-body">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="executor_type">Executor type</label>
<select id="executor_type" name="executor_type" class="wf-select" onchange="updateExecutorField()">
<option value="shell" {% if form_executor_type == "shell" %}selected{% endif %}>Shell command</option>
<option value="script" {% if form_executor_type == "script" %}selected{% endif %}>Executable script</option>
<option value="javascript" {% if form_executor_type == "javascript" %}selected{% endif %}>JavaScript script</option>
<option value="python" {% if form_executor_type == "python" %}selected{% endif %}>Python script</option>
<option value="http" {% if form_executor_type == "http" %}selected{% endif %}>HTTP request</option>
</select>
<span class="wf-field-hint">Choose how sendword should run this hook.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" id="command_label" for="command">Shell command</label>
<input type="text" id="command" name="command" value="{{ form_command }}" required placeholder="make deploy" class="wf-input">
<span class="wf-field-hint" id="command_hint">Shell commands may use payload interpolation like {{ "{{ action }}" }}.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="cwd">Working directory</label>
<input type="text" id="cwd" name="cwd" value="{{ form_cwd }}" placeholder="/opt/app (optional)" class="wf-input">
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="timeout">Timeout</label>
<input type="text" id="timeout" name="timeout" value="{{ form_timeout }}" placeholder="30s" class="wf-input">
<span class="wf-field-hint">Duration with unit, e.g. 30s, 2m, 1h. Leave blank for default.</span>
</div>
<div class="wf-field">
<label class="wf-label" for="env_text">Environment variables</label>
<textarea id="env_text" name="env_text" rows="4" placeholder="KEY=VALUE" class="wf-textarea" spellcheck="false">{{ form_env_text }}</textarea>
<span class="wf-field-hint">One variable per line, in KEY=VALUE format.</span>
</div>
</div>
</div>
{# --- Payload Schema --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">PAYLOAD SCHEMA</span></div>
<div class="wf-panel-body">
<div class="wf-field">
<label class="wf-label" for="payload_text">Field definitions</label>
<textarea id="payload_text" name="payload_text" rows="4" placeholder="action:string:required tag:string count:number:required" class="wf-textarea" spellcheck="false">{{ form_payload_text }}</textarea>
<span class="wf-field-hint">One field per line: name:type or name:type:required. Types: string, number, boolean, object, array.</span>
</div>
</div>
</div>
{# --- Trigger Rules --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">TRIGGER RULES</span></div>
<div class="wf-panel-body">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="trigger_filters_text">Payload filters</label>
<textarea id="trigger_filters_text" name="trigger_filters_text" rows="3" placeholder="action:equals:deploy env:contains:prod" class="wf-textarea" spellcheck="false">{{ form_trigger_filters_text }}</textarea>
<span class="wf-field-hint">One filter per line: field:operator:value. Operators: equals, not_equals, contains, regex, gt, lt, gte, lte, exists.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="trigger_windows_text">Time windows (UTC)</label>
<textarea id="trigger_windows_text" name="trigger_windows_text" rows="2" placeholder="Mon,Tue,Wed,Thu,Fri:09:00-17:00" class="wf-textarea" spellcheck="false">{{ form_trigger_windows_text }}</textarea>
<span class="wf-field-hint">One window per line: Days:HH:MM-HH:MM. Leave blank to allow at any time.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="trigger_cooldown">Cooldown</label>
<input type="text" id="trigger_cooldown" name="trigger_cooldown" value="{{ form_trigger_cooldown }}" placeholder="5m" class="wf-input">
<span class="wf-field-hint">Duration with unit, e.g. 30s, 5m, 1h.</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="wf-field">
<label class="wf-label" for="trigger_rate_max">Rate limit max</label>
<input type="number" id="trigger_rate_max" name="trigger_rate_max" value="{{ form_trigger_rate_max }}" min="1" placeholder="10" class="wf-input">
</div>
<div class="wf-field">
<label class="wf-label" for="trigger_rate_window">Rate limit window</label>
<input type="text" id="trigger_rate_window" name="trigger_rate_window" value="{{ form_trigger_rate_window }}" placeholder="1h" class="wf-input">
</div>
</div>
</div>
</div>
{# --- Retry --- #}
<div class="wf-panel" style="margin-bottom: 24px;">
<div class="wf-panel-head"><span class="wf-panel-title">RETRY</span></div>
<div class="wf-panel-body">
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="retry_count">Retry count</label>
<input type="number" id="retry_count" name="retry_count" value="{{ form_retry_count }}" min="0" placeholder="0" class="wf-input">
<span class="wf-field-hint">0 = no retries.</span>
</div>
<div class="wf-field" style="margin-bottom: 16px;">
<label class="wf-label" for="retry_backoff">Backoff strategy</label>
<select id="retry_backoff" name="retry_backoff" class="wf-select">
<option value="none" {% if form_retry_backoff == "none" %}selected{% endif %}>None</option>
<option value="linear" {% if form_retry_backoff == "linear" %}selected{% endif %}>Linear</option>
<option value="exponential" {% if form_retry_backoff == "exponential" %}selected{% endif %}>Exponential</option>
</select>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="wf-field">
<label class="wf-label" for="retry_initial_delay">Initial delay</label>
<input type="text" id="retry_initial_delay" name="retry_initial_delay" value="{{ form_retry_initial_delay }}" placeholder="1s" class="wf-input">
</div>
<div class="wf-field">
<label class="wf-label" for="retry_max_delay">Max delay</label>
<input type="text" id="retry_max_delay" name="retry_max_delay" value="{{ form_retry_max_delay }}" placeholder="60s" class="wf-input">
</div>
</div>
</div>
</div>
{# --- Actions --- #}
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">
<a class="wf-btn" href="{% if is_new %}/{% else %}/hooks/{{ slug }}{% endif %}">CANCEL</a>
<button type="submit" class="wf-btn primary">{% if is_new %}CREATE HOOK{% else %}SAVE CHANGES{% endif %}</button>
</div>
</form>
<script>
function toggleAuthFields() {
var mode = document.getElementById('auth_mode').value;
document.getElementById('bearer-fields').style.display = mode === 'bearer' ? '' : 'none';
document.getElementById('hmac-fields').style.display = mode === 'hmac' ? '' : 'none';
}
function updateExecutorField() {
var type = document.getElementById('executor_type').value;
var command = document.getElementById('command');
var label = document.getElementById('command_label');
var hint = document.getElementById('command_hint');
var copy = {
shell: {
label: 'Shell command',
placeholder: 'make deploy',
hint: 'Shell commands may use payload interpolation like {{ "{{ action }}" }}.'
},
script: {
label: 'Script path',
placeholder: 'data/scripts/deploy.sh',
hint: 'Executable scripts are run directly and need a shebang plus executable permissions.'
},
javascript: {
label: 'JavaScript path',
placeholder: 'data/scripts/deploy.js',
hint: 'JavaScript scripts run with node and can read payload fields from process.env.'
},
python: {
label: 'Python path',
placeholder: 'data/scripts/deploy.py',
hint: 'Python scripts run with python3, then python, and can read payload fields from os.environ.'
},
http: {
label: 'HTTP URL',
placeholder: 'https://example.com/webhook',
hint: 'HTTP executors are view-only in this form and cannot be saved here.'
}
};
var selected = copy[type] || copy.shell;
label.textContent = selected.label;
command.placeholder = selected.placeholder;
hint.textContent = selected.hint;
}
toggleAuthFields();
updateExecutorField();
</script>
{% endblock %}