check / check (push) Successful in 3m20s
A refused save on the target edit page now shows the edit form again, with the reason above it and every value submitted, instead of a bare text page; the status codes are unchanged. The webhook edit page keeps the submitted name, description and retention the same way, while the page still reports the stored retention. Target edits are validated by setTargetFromForm, which newTarget now uses too, so the add and edit forms accept and refuse the same things. An empty max_retries keeps the target's own count. The browser test also saves both edit pages with refused values. Its seeding and its table of target types move into helpers, leaving the test function a plain list of check calls. Model: opus-5-5
84 lines
4.7 KiB
HTML
84 lines
4.7 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "title"}}Edit {{.Target.Name}} - Webhooker{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="max-w-2xl mx-auto px-6 py-8">
|
|
<div class="mb-6">
|
|
<a href="/hook/{{.Webhook.ID}}" class="btn-small">← Back to {{.Webhook.Name}}</a>
|
|
<h1 class="text-2xl font-medium text-gray-900 mt-2">Edit Target</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Type: {{.Target.Type}}. A target's type cannot be changed; create a new target to deliver a different way.</p>
|
|
</div>
|
|
|
|
<div class="card p-6">
|
|
{{if .Error}}
|
|
<div class="alert-error">{{.Error}}</div>
|
|
{{end}}
|
|
|
|
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
|
<div class="mb-6 rounded-md bg-gray-50 p-4 text-sm text-gray-700">
|
|
This form shows the target's destination in full, including any credential carried in its URL or headers. It is the only page that does; everywhere else the value is masked.
|
|
</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/hook/{{.Webhook.ID}}/targets/{{.Target.ID}}/edit" class="space-y-6">
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|
|
|
<div class="form-group">
|
|
<label for="name" class="label">Name</label>
|
|
<input type="text" id="name" name="name" value="{{.TargetForm.Name}}" required class="input">
|
|
</div>
|
|
|
|
{{if eq .Target.Type "http"}}
|
|
<div class="form-group">
|
|
<label for="url" class="label">Destination URL</label>
|
|
<input type="url" id="url" name="url" value="{{.TargetForm.URL}}" required class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Revalidated on save; destinations that resolve to private or link-local addresses are rejected.</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="headers" class="label">Headers</label>
|
|
<textarea id="headers" name="headers" rows="4" class="input" placeholder="Authorization: Bearer ...">{{.TargetForm.Headers}}</textarea>
|
|
<p class="text-xs text-gray-500 mt-1">One <code>Name: value</code> per line, sent with every delivery. Leave blank for none. <code>Host</code>, <code>Content-Length</code>, <code>Transfer-Encoding</code>, <code>Connection</code>, <code>Trailer</code> and <code>User-Agent</code> are set by the delivery engine and are rejected here rather than silently ignored. Headers set here are dropped if a redirect leaves the destination's own origin, so a credential cannot follow one to another host.</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="timeout" class="label">Timeout (seconds)</label>
|
|
<input type="number" id="timeout" name="timeout" value="{{.TargetForm.Timeout}}" min="0" max="{{.MaxTimeout}}" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Per-request timeout, at most {{.MaxTimeout}} seconds. Leave blank to use the default.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq .Target.Type "slack"}}
|
|
<div class="form-group">
|
|
<label for="url" class="label">Webhook URL</label>
|
|
<input type="url" id="url" name="url" value="{{.TargetForm.URL}}" required class="input">
|
|
<p class="text-xs text-gray-500 mt-1">Slack or Mattermost incoming webhook URL. Revalidated on save.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq .Target.Type "database"}}
|
|
<div class="form-group">
|
|
<label for="expiry" class="label">Archive Expiry</label>
|
|
<input type="text" id="expiry" name="expiry" value="{{.TargetForm.Expiry}}" placeholder="never" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">"never" (the default when blank) keeps archived rows forever, or a Go duration like "720h" prunes older rows.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if or (eq .Target.Type "http") (eq .Target.Type "slack")}}
|
|
<div class="form-group">
|
|
<label for="max_retries" class="label">Max retries</label>
|
|
<input type="number" id="max_retries" name="max_retries" value="{{.TargetForm.MaxRetries}}" min="0" max="20" class="input">
|
|
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="flex gap-3">
|
|
<button type="submit" class="btn-primary">Save Changes</button>
|
|
<a href="/hook/{{.Webhook.ID}}" class="btn-secondary">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{{end}}
|