dlq.html 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>broad-announce · DLQ</title>
  6. <style>
  7. :root { color-scheme: light dark; }
  8. body {
  9. font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  10. margin: 0; padding: 24px;
  11. background: #fafafa; color: #111;
  12. }
  13. @media (prefers-color-scheme: dark) {
  14. body { background: #0d0f12; color: #e4e4e4; }
  15. table { border-color: #2a2e35; }
  16. th { background: #161a20; }
  17. tr:nth-child(even) { background: #131820; }
  18. a, a:visited { color: #6ea8fe; }
  19. .filters { background: #161a20; border-color: #2a2e35; }
  20. }
  21. h1 { margin: 0 0 12px; font-size: 22px; font-weight: 600; }
  22. .sub { color: #6b6b6b; font-size: 13px; margin-bottom: 16px; }
  23. .filters {
  24. display: flex; gap: 8px; align-items: end; flex-wrap: wrap;
  25. padding: 10px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 16px;
  26. }
  27. .filters label { display: flex; flex-direction: column; font-size: 12px; }
  28. .filters input { padding: 4px 6px; font-size: 13px; min-width: 140px; }
  29. .filters button { padding: 5px 12px; font-size: 13px; }
  30. table { width: 100%; border-collapse: collapse; font-size: 13px; }
  31. th, td { padding: 6px 8px; border-bottom: 1px solid #eee; text-align: left; vertical-align: top; }
  32. th { background: #f3f3f3; font-weight: 600; }
  33. td.id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #888; }
  34. td.err { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; max-width: 280px; word-break: break-all; }
  35. td.actions { white-space: nowrap; }
  36. .btn { padding: 3px 9px; font-size: 12px; border-radius: 4px; border: 1px solid #888; background: transparent; cursor: pointer; color: inherit; }
  37. .btn:hover { background: rgba(0,0,0,.06); }
  38. .btn-primary { border-color: #2e7d32; color: #2e7d32; }
  39. .btn-primary:hover { background: rgba(46,125,50,.1); }
  40. .btn-danger { border-color: #b71c1c; color: #b71c1c; }
  41. .btn-danger:hover { background: rgba(183,28,28,.1); }
  42. .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 11px; background: #eee; color: #555; }
  43. .empty { padding: 40px; text-align: center; color: #888; font-style: italic; }
  44. .nav { margin-top: 12px; font-size: 12px; }
  45. .nav a { color: #1976d2; text-decoration: none; margin-right: 8px; }
  46. .nav a:hover { text-decoration: underline; }
  47. details { margin-top: 4px; }
  48. details pre {
  49. font-family: ui-monospace, monospace; font-size: 11px; max-width: 480px;
  50. max-height: 200px; overflow: auto; padding: 6px; background: rgba(0,0,0,.04); border-radius: 3px;
  51. }
  52. .toast {
  53. position: fixed; bottom: 16px; right: 16px; padding: 10px 14px;
  54. background: #323232; color: #fff; border-radius: 4px;
  55. font-size: 13px; opacity: 0; transition: opacity .2s;
  56. pointer-events: none;
  57. }
  58. .toast.show { opacity: 1; }
  59. .toast.error { background: #b71c1c; }
  60. </style>
  61. </head>
  62. <body>
  63. <h1>broad-announce · DLQ</h1>
  64. <p class="sub">
  65. {{- if .HasFilters }}
  66. Filtered:
  67. {{- if .Filters.CompanyID }} <span class="pill">company={{.Filters.CompanyID}}</span>{{- end }}
  68. {{- if .Filters.Channel }} <span class="pill">channel={{.Filters.Channel}}</span>{{- end }}
  69. {{- if .Filters.AlertID }} <span class="pill">alert={{.Filters.AlertID}}</span>{{- end }}
  70. ·
  71. {{- end }}
  72. {{if .ShowAll}}showing all (incl. discarded){{else}}hiding discarded{{end}} · last 30d · {{len .Rows}} row(s)
  73. </p>
  74. <form class="filters" method="get" action="/dlq">
  75. <label>company_id
  76. <input type="text" name="company_id" value="{{.Filters.CompanyID}}">
  77. </label>
  78. <label>channel
  79. <input type="text" name="channel" value="{{.Filters.Channel}}">
  80. </label>
  81. <label>alert_id
  82. <input type="text" name="alert_id" value="{{.Filters.AlertID}}">
  83. </label>
  84. <label style="flex-direction:row;align-items:center;gap:4px">
  85. <input type="checkbox" name="include" value="all" {{if .ShowAll}}checked{{end}}> show discarded
  86. </label>
  87. <button type="submit">Apply</button>
  88. <a href="/dlq" class="btn">Reset</a>
  89. </form>
  90. {{if .Rows}}
  91. <table>
  92. <thead>
  93. <tr>
  94. <th>id</th>
  95. <th>created</th>
  96. <th>company</th>
  97. <th>channel</th>
  98. <th>alert</th>
  99. <th>individual</th>
  100. <th>attempts</th>
  101. <th>last error</th>
  102. <th>actions</th>
  103. </tr>
  104. </thead>
  105. <tbody>
  106. {{range .Rows}}
  107. <tr>
  108. <td class="id">#{{.ID}}</td>
  109. <td>{{.CreatedAt.Format "2006-01-02 15:04:05 UTC"}}</td>
  110. <td>{{.CompanyID}}</td>
  111. <td><span class="pill">{{.Channel}}</span></td>
  112. <td class="id">{{.AlertID}}</td>
  113. <td class="id">{{.IndividualID}}</td>
  114. <td>{{.Attempts}}</td>
  115. <td class="err">{{.LastError}}</td>
  116. <td class="actions">
  117. {{if not .Discarded}}
  118. <button class="btn btn-primary" onclick="dlqAction({{.ID}}, 'replay', this)">replay</button>
  119. <button class="btn btn-danger" onclick="dlqAction({{.ID}}, 'discard', this)">discard</button>
  120. {{else}}
  121. <span class="pill">discarded{{if .DiscardedBy}} by {{.DiscardedBy}}{{end}}</span>
  122. {{end}}
  123. <a class="btn" href="/v1/dlq/{{.ID}}" target="_blank">json</a>
  124. </td>
  125. </tr>
  126. {{end}}
  127. </tbody>
  128. </table>
  129. {{else}}
  130. <div class="empty">No DLQ rows match the current filters. 🎉</div>
  131. {{end}}
  132. <div class="nav">
  133. <a href="/v1/dlq?limit=100">raw JSON</a> ·
  134. <a href="/metrics">metrics</a> ·
  135. <a href="/v1/ping">ping</a>
  136. </div>
  137. <div id="toast" class="toast"></div>
  138. <script>
  139. function showToast(msg, isError) {
  140. var t = document.getElementById("toast");
  141. t.textContent = msg;
  142. t.className = "toast show" + (isError ? " error" : "");
  143. setTimeout(function() { t.className = "toast"; }, 2400);
  144. }
  145. function dlqAction(id, action, btn) {
  146. if (!confirm(action + " DLQ row #" + id + "?")) return;
  147. btn.disabled = true;
  148. fetch("/v1/dlq/" + id + "/" + action, { method: "POST" })
  149. .then(function(r) {
  150. if (!r.ok) {
  151. return r.text().then(function(t) {
  152. throw new Error("HTTP " + r.status + ": " + t);
  153. });
  154. }
  155. return r.json();
  156. })
  157. .then(function(j) {
  158. showToast(action + " #" + id + " ok", false);
  159. setTimeout(function() { location.reload(); }, 600);
  160. })
  161. .catch(function(e) {
  162. showToast("error: " + e.message, true);
  163. btn.disabled = false;
  164. });
  165. }
  166. </script>
  167. </body>
  168. </html>