| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>broad-announce · DLQ</title>
- <style>
- :root { color-scheme: light dark; }
- body {
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
- margin: 0; padding: 24px;
- background: #fafafa; color: #111;
- }
- @media (prefers-color-scheme: dark) {
- body { background: #0d0f12; color: #e4e4e4; }
- table { border-color: #2a2e35; }
- th { background: #161a20; }
- tr:nth-child(even) { background: #131820; }
- a, a:visited { color: #6ea8fe; }
- .filters { background: #161a20; border-color: #2a2e35; }
- }
- h1 { margin: 0 0 12px; font-size: 22px; font-weight: 600; }
- .sub { color: #6b6b6b; font-size: 13px; margin-bottom: 16px; }
- .filters {
- display: flex; gap: 8px; align-items: end; flex-wrap: wrap;
- padding: 10px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 16px;
- }
- .filters label { display: flex; flex-direction: column; font-size: 12px; }
- .filters input { padding: 4px 6px; font-size: 13px; min-width: 140px; }
- .filters button { padding: 5px 12px; font-size: 13px; }
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
- th, td { padding: 6px 8px; border-bottom: 1px solid #eee; text-align: left; vertical-align: top; }
- th { background: #f3f3f3; font-weight: 600; }
- td.id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #888; }
- td.err { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; max-width: 280px; word-break: break-all; }
- td.actions { white-space: nowrap; }
- .btn { padding: 3px 9px; font-size: 12px; border-radius: 4px; border: 1px solid #888; background: transparent; cursor: pointer; color: inherit; }
- .btn:hover { background: rgba(0,0,0,.06); }
- .btn-primary { border-color: #2e7d32; color: #2e7d32; }
- .btn-primary:hover { background: rgba(46,125,50,.1); }
- .btn-danger { border-color: #b71c1c; color: #b71c1c; }
- .btn-danger:hover { background: rgba(183,28,28,.1); }
- .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 11px; background: #eee; color: #555; }
- .empty { padding: 40px; text-align: center; color: #888; font-style: italic; }
- .nav { margin-top: 12px; font-size: 12px; }
- .nav a { color: #1976d2; text-decoration: none; margin-right: 8px; }
- .nav a:hover { text-decoration: underline; }
- details { margin-top: 4px; }
- details pre {
- font-family: ui-monospace, monospace; font-size: 11px; max-width: 480px;
- max-height: 200px; overflow: auto; padding: 6px; background: rgba(0,0,0,.04); border-radius: 3px;
- }
- .toast {
- position: fixed; bottom: 16px; right: 16px; padding: 10px 14px;
- background: #323232; color: #fff; border-radius: 4px;
- font-size: 13px; opacity: 0; transition: opacity .2s;
- pointer-events: none;
- }
- .toast.show { opacity: 1; }
- .toast.error { background: #b71c1c; }
- </style>
- </head>
- <body>
- <h1>broad-announce · DLQ</h1>
- <p class="sub">
- {{- if .HasFilters }}
- Filtered:
- {{- if .Filters.CompanyID }} <span class="pill">company={{.Filters.CompanyID}}</span>{{- end }}
- {{- if .Filters.Channel }} <span class="pill">channel={{.Filters.Channel}}</span>{{- end }}
- {{- if .Filters.AlertID }} <span class="pill">alert={{.Filters.AlertID}}</span>{{- end }}
- ·
- {{- end }}
- {{if .ShowAll}}showing all (incl. discarded){{else}}hiding discarded{{end}} · last 30d · {{len .Rows}} row(s)
- </p>
- <form class="filters" method="get" action="/dlq">
- <label>company_id
- <input type="text" name="company_id" value="{{.Filters.CompanyID}}">
- </label>
- <label>channel
- <input type="text" name="channel" value="{{.Filters.Channel}}">
- </label>
- <label>alert_id
- <input type="text" name="alert_id" value="{{.Filters.AlertID}}">
- </label>
- <label style="flex-direction:row;align-items:center;gap:4px">
- <input type="checkbox" name="include" value="all" {{if .ShowAll}}checked{{end}}> show discarded
- </label>
- <button type="submit">Apply</button>
- <a href="/dlq" class="btn">Reset</a>
- </form>
- {{if .Rows}}
- <table>
- <thead>
- <tr>
- <th>id</th>
- <th>created</th>
- <th>company</th>
- <th>channel</th>
- <th>alert</th>
- <th>individual</th>
- <th>attempts</th>
- <th>last error</th>
- <th>actions</th>
- </tr>
- </thead>
- <tbody>
- {{range .Rows}}
- <tr>
- <td class="id">#{{.ID}}</td>
- <td>{{.CreatedAt.Format "2006-01-02 15:04:05 UTC"}}</td>
- <td>{{.CompanyID}}</td>
- <td><span class="pill">{{.Channel}}</span></td>
- <td class="id">{{.AlertID}}</td>
- <td class="id">{{.IndividualID}}</td>
- <td>{{.Attempts}}</td>
- <td class="err">{{.LastError}}</td>
- <td class="actions">
- {{if not .Discarded}}
- <button class="btn btn-primary" onclick="dlqAction({{.ID}}, 'replay', this)">replay</button>
- <button class="btn btn-danger" onclick="dlqAction({{.ID}}, 'discard', this)">discard</button>
- {{else}}
- <span class="pill">discarded{{if .DiscardedBy}} by {{.DiscardedBy}}{{end}}</span>
- {{end}}
- <a class="btn" href="/v1/dlq/{{.ID}}" target="_blank">json</a>
- </td>
- </tr>
- {{end}}
- </tbody>
- </table>
- {{else}}
- <div class="empty">No DLQ rows match the current filters. 🎉</div>
- {{end}}
- <div class="nav">
- <a href="/v1/dlq?limit=100">raw JSON</a> ·
- <a href="/metrics">metrics</a> ·
- <a href="/v1/ping">ping</a>
- </div>
- <div id="toast" class="toast"></div>
- <script>
- function showToast(msg, isError) {
- var t = document.getElementById("toast");
- t.textContent = msg;
- t.className = "toast show" + (isError ? " error" : "");
- setTimeout(function() { t.className = "toast"; }, 2400);
- }
- function dlqAction(id, action, btn) {
- if (!confirm(action + " DLQ row #" + id + "?")) return;
- btn.disabled = true;
- fetch("/v1/dlq/" + id + "/" + action, { method: "POST" })
- .then(function(r) {
- if (!r.ok) {
- return r.text().then(function(t) {
- throw new Error("HTTP " + r.status + ": " + t);
- });
- }
- return r.json();
- })
- .then(function(j) {
- showToast(action + " #" + id + " ok", false);
- setTimeout(function() { location.reload(); }, 600);
- })
- .catch(function(e) {
- showToast("error: " + e.message, true);
- btn.disabled = false;
- });
- }
- </script>
- </body>
- </html>
|