fix(web/webhooks): match LiveLogs page-header convention

The webhooks page used a bespoke .webhooks-header wrapper that didn't
line up with the rest of the dashboard (Fleet / Logs / Swarm all use
the .<page>-root + .page-header + .page-title-group + .actions
pattern). Swapped to that convention:

- .webhooks-root wrapper, matching .logs-root / .fleet-root spacing.
- H1 "WEBHOOKS" in .page-title-group; subtitle shows
  `N CONFIGURED · M ENABLED [· K FAILING] [· L INSECURE]` in
  .page-sub, same voice as the LOGS stream summary.
- Actions (CREATE WEBHOOK, DELETE SELECTED) sit in .actions.
- Table lives in a proper .logs-section shell with a .section-header
  carrying the Webhook icon + "SUBSCRIPTIONS" title.
- All scoped button overrides (violet/alert/warn/ghost) copied from
  the LiveLogs scope so theme switches behave identically.

Also improve error messaging: extractErrorDetail now maps 401 to
"Session expired" and 403 to "Insufficient permissions (admin only)"
instead of falling through to the generic "Failed to load webhooks".
Helps users who hit the page as viewer or with a stale token see why
it failed.
This commit is contained in:
2026-04-24 16:11:20 -04:00
parent 59c405d9e5
commit 4d10eba7a7
2 changed files with 359 additions and 226 deletions

View File

@@ -1,88 +1,125 @@
.webhooks-page {
/* Webhooks page — mirrors the .logs-root / .fleet-root / .swarm-root shape. */
.webhooks-root {
display: flex;
flex-direction: column;
gap: 24px;
}
.webhooks-header {
.webhooks-root .page-title-group {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
flex-direction: column;
gap: 6px;
}
.webhooks-header h2 {
.webhooks-root .page-header h1 {
font-size: 1.3rem;
letter-spacing: 4px;
font-weight: 700;
margin: 0;
font-size: 0.95rem;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-color);
color: var(--matrix);
}
.webhooks-header-actions {
display: flex;
gap: 12px;
}
.webhooks-warning-banner {
background: rgba(224, 160, 64, 0.1);
border: 1px solid var(--warn);
color: var(--warn);
padding: 10px 16px;
font-size: 0.78rem;
.webhooks-root .page-sub {
font-size: 0.7rem;
opacity: 0.5;
letter-spacing: 1px;
}
.webhooks-root .page-header .actions {
display: flex;
gap: 10px;
align-items: center;
}
/* Canonical buttons (copy LiveLogs' scoped rules so theme/accent behaves). */
.webhooks-root .btn {
cursor: pointer;
background: transparent;
border: 1px solid var(--matrix);
color: var(--matrix);
padding: 7px 14px;
font-family: inherit;
font-size: 0.78rem;
letter-spacing: 1.5px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.webhooks-root .btn:hover {
background: var(--matrix);
color: #000;
box-shadow: var(--matrix-glow);
}
.webhooks-root .btn.violet { border-color: var(--violet); color: var(--violet); }
.webhooks-root .btn.violet:hover { background: var(--violet); color: #000; box-shadow: var(--violet-glow); }
.webhooks-root .btn.alert { border-color: var(--alert, #ff4d4d); color: var(--alert, #ff4d4d); }
.webhooks-root .btn.alert:hover { background: var(--alert, #ff4d4d); color: #000; box-shadow: 0 0 10px rgba(255, 77, 77, 0.5); }
.webhooks-root .btn.warn { border-color: var(--warn, #e0a040); color: var(--warn, #e0a040); }
.webhooks-root .btn.warn:hover { background: var(--warn, #e0a040); color: #000; box-shadow: 0 0 10px rgba(224, 160, 64, 0.5); }
.webhooks-root .btn.ghost { border-color: var(--border-color); color: var(--matrix); opacity: 0.7; }
.webhooks-root .btn.ghost:hover { opacity: 1; border-color: var(--matrix); background: transparent; box-shadow: var(--matrix-glow); }
.webhooks-root .btn:disabled { opacity: 0.3; cursor: not-allowed; }
.webhooks-root .webhooks-error {
margin: 0;
}
.webhooks-root .webhooks-warning-banner {
background: rgba(224, 160, 64, 0.08);
border: 1px solid var(--warn, #e0a040);
color: var(--warn, #e0a040);
padding: 10px 14px;
font-size: 0.7rem;
letter-spacing: 1.5px;
display: flex;
align-items: center;
gap: 10px;
}
.webhooks-empty {
padding: 48px;
/* Table container — reuse the .logs-section shell from Dashboard.css. */
.webhooks-root .webhooks-empty {
padding: 40px;
text-align: center;
opacity: 0.5;
font-size: 0.82rem;
font-size: 0.78rem;
letter-spacing: 1.5px;
border: 1px dashed var(--border-color);
}
.webhooks-table-wrap {
.webhooks-root .webhooks-table-wrap {
overflow-x: auto;
}
.webhooks-table {
.webhooks-root .webhooks-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
font-size: 0.78rem;
}
.webhooks-table thead {
font-size: 0.65rem;
.webhooks-root .webhooks-table thead {
font-size: 0.62rem;
letter-spacing: 1.5px;
opacity: 0.6;
}
.webhooks-table th,
.webhooks-table td {
padding: 10px 12px;
.webhooks-root .webhooks-table th,
.webhooks-root .webhooks-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
vertical-align: middle;
}
.webhooks-table tbody tr:hover {
.webhooks-root .webhooks-table tbody tr:hover {
background: rgba(0, 255, 65, 0.03);
}
.webhooks-table .col-check {
width: 32px;
}
.webhooks-root .webhooks-table .col-check { width: 28px; }
.webhooks-root .webhooks-table .col-actions { width: 140px; }
.webhooks-table .col-actions {
width: 140px;
}
.wh-url-cell {
.webhooks-root .wh-url-cell {
font-family: var(--font-mono);
max-width: 260px;
overflow: hidden;
@@ -90,66 +127,86 @@
white-space: nowrap;
}
.wh-chip {
font-size: 0.68rem;
padding: 2px 6px;
/* Chips — mirror the canonical chip spec from UI-Things.md. */
.webhooks-root .wh-chip {
font-size: 0.66rem;
padding: 2px 7px;
border: 1px solid var(--accent-tint-30, rgba(0, 255, 65, 0.3));
background: var(--accent-tint-10, rgba(0, 255, 65, 0.1));
color: var(--accent-color);
color: var(--accent);
letter-spacing: 0.5px;
font-family: var(--font-mono);
margin-right: 4px;
display: inline-block;
}
.wh-chip.status-disabled {
.webhooks-root .wh-chip.status-disabled {
border-color: var(--border-color);
color: var(--text-color);
background: transparent;
opacity: 0.6;
}
.wh-chip.status-fail {
.webhooks-root .wh-chip.status-fail {
border-color: var(--alert, #ff4d4d);
background: rgba(255, 77, 77, 0.12);
color: var(--alert, #ff4d4d);
}
.wh-chip.status-warn {
.webhooks-root .wh-chip.status-warn {
border-color: var(--warn, #e0a040);
background: rgba(224, 160, 64, 0.1);
color: var(--warn, #e0a040);
}
.wh-actions {
.webhooks-root .wh-actions {
display: flex;
gap: 6px;
}
/* Inline create / edit form, rendered as an expanded row. */
.wh-form-row td {
padding: 20px 12px;
background: rgba(0, 0, 0, 0.2);
/* Inline form row (create + edit). */
.webhooks-root .wh-form-row td {
padding: 20px 20px;
background: rgba(0, 0, 0, 0.25);
}
.wh-form-grid {
.webhooks-root .wh-form-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 14px;
max-width: 900px;
grid-template-columns: 160px 1fr;
gap: 14px 16px;
max-width: 920px;
}
.wh-form-grid label {
font-size: 0.65rem;
letter-spacing: 1px;
.webhooks-root .wh-form-grid label {
font-size: 0.62rem;
letter-spacing: 1.5px;
opacity: 0.6;
align-self: center;
text-transform: uppercase;
}
.wh-form-grid input[type="text"],
.wh-form-grid input[type="url"],
.wh-form-grid input[type="password"],
.wh-form-grid textarea {
.webhooks-root .wh-form-title {
grid-column: 1 / -1;
font-size: 0.7rem;
color: var(--violet);
letter-spacing: 2px;
opacity: 1;
margin: 0 0 4px 0;
}
.webhooks-root .wh-form-hint {
opacity: 0.5;
font-weight: normal;
letter-spacing: 0.5px;
text-transform: none;
}
.webhooks-root .wh-form-grid input[type="text"],
.webhooks-root .wh-form-grid input[type="url"],
.webhooks-root .wh-form-grid input[type="password"],
.webhooks-root .wh-form-grid textarea {
background: #0d1117;
border: 1px solid var(--border-color);
color: var(--text-color);
@@ -160,36 +217,47 @@
box-sizing: border-box;
}
.wh-form-grid textarea {
min-height: 80px;
.webhooks-root .wh-form-grid textarea {
min-height: 72px;
font-family: var(--font-mono);
}
.wh-form-grid .wh-checkbox-group {
.webhooks-root .wh-form-grid input:focus,
.webhooks-root .wh-form-grid textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: var(--accent-glow, 0 0 10px rgba(0, 255, 65, 0.5));
}
.webhooks-root .wh-checkbox-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
}
.wh-form-grid .wh-checkbox-group label {
font-size: 0.78rem;
letter-spacing: 0.5px;
.webhooks-root .wh-checkbox-group label {
font-size: 0.72rem;
letter-spacing: 1px;
opacity: 1;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
text-transform: none;
}
.wh-form-buttons {
.webhooks-root .wh-form-buttons {
grid-column: 1 / -1;
display: flex;
gap: 10px;
justify-content: flex-end;
padding-top: 8px;
padding-top: 10px;
border-top: 1px dashed var(--border-color);
}
/* Secret-modal — one-shot display after create. */
.wh-secret-modal-backdrop {
position: fixed;
inset: 0;
@@ -218,9 +286,13 @@
}
.wh-secret-modal .wh-secret-warn {
color: var(--warn);
font-size: 0.78rem;
color: var(--warn, #e0a040);
font-size: 0.7rem;
letter-spacing: 1px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.wh-secret-modal .wh-secret-value {
@@ -237,3 +309,22 @@
gap: 10px;
justify-content: flex-end;
}
.wh-secret-modal .btn {
cursor: pointer;
background: transparent;
border: 1px solid var(--matrix);
color: var(--matrix);
padding: 7px 14px;
font-family: inherit;
font-size: 0.78rem;
letter-spacing: 1.5px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.wh-secret-modal .btn.violet { border-color: var(--violet); color: var(--violet); }
.wh-secret-modal .btn.violet:hover { background: var(--violet); color: #000; box-shadow: var(--violet-glow); }
.wh-secret-modal .btn.ghost { border-color: var(--border-color); color: var(--matrix); opacity: 0.7; }
.wh-secret-modal .btn.ghost:hover { opacity: 1; border-color: var(--matrix); background: transparent; box-shadow: var(--matrix-glow); }