Files
BetterDesk/web-nodejs/public/css/scaling.css
T
UNITRONIX a23b860d41 fix(scaling): make Relay Nodes Add/Edit actually save (#119)
Three independent bugs combined to make 'Add Relay' silently do nothing:

1. saveRelay() in public/js/scaling.js used raw fetch() without the panel's
   CSRF token, so every POST/PUT/DELETE was rejected by the CSRF middleware.
2. The catch block swallowed every error and the modal closed regardless of
   the response code, so the user never saw the rejection.
3. The relay/rules routes proxied to /scaling/relays on the Go server, but
   the Go server doesn't (yet) implement those endpoints — every request
   ended up as a 404 and the panel's relay table stayed empty forever.

Changes:

- routes/scaling.routes.js: drop the dead Go proxy, persist relay-node
  metadata and assignment rules in the existing 'settings' table
  (auth.db / DATABASE_URL). Strict input validation (host:port,
  Unicode-safe name, ranges for sessions/bandwidth/priority), 409 on
  duplicate addresses, capped at 100 relays / 200 rules. Health and
  metrics endpoints now return explicit 'live telemetry not yet
  available' placeholders instead of 500s.
- public/js/scaling.js: every mutating fetch() now sends x-csrf-token,
  validates inputs with a toast, parses JSON error bodies, only closes
  the modal and reloads on success. Falls back to alert() if the toast
  service is missing so users are never left guessing.
- views/scaling.ejs: info banner above the Relay Nodes table making
  clear that the list is operator-side metadata and that actual relay
  routing is configured via RELAY_SERVERS on the BetterDesk server.
- public/css/scaling.css: styles for the new info banner.
- lang/en.json, lang/pl.json: i18n keys for the banner, validation
  messages and success/failure toasts.

Note: the reporter also tried RELAY=... — only RELAY_SERVERS is read
by the Go server (see config/config.go:160). Worth surfacing in docs
in a follow-up.

Reported-by: @Elektrik1
2026-05-01 22:24:57 +02:00

309 lines
12 KiB
CSS

/* scaling.css — Scaling & Relay Nodes Management */
.scaling-page { max-width: 1400px; margin: 0 auto; padding: 0 16px; }
/* Tab bar */
.scaling-tabs {
display: flex; gap: 4px; margin-bottom: 20px;
border-bottom: 1px solid var(--border-primary, #2d333b);
padding-bottom: 0;
}
.scaling-tab {
display: flex; align-items: center; gap: 6px;
padding: 10px 18px; border: none; background: none;
color: var(--text-secondary, #8b949e); cursor: pointer;
font-size: 13px; font-weight: 500;
border-bottom: 2px solid transparent;
transition: color .15s, border-color .15s;
}
.scaling-tab:hover { color: var(--text-primary, #e6edf3); }
.scaling-tab.active {
color: var(--accent-blue, #58a6ff);
border-bottom-color: var(--accent-blue, #58a6ff);
}
.scaling-tab .material-icons { font-size: 18px; }
/* Panels */
.scaling-panel { animation: fadeIn .2s ease; }
/* Toolbar */
.scaling-toolbar {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
}
.scaling-toolbar-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.scaling-toolbar-right { display: flex; gap: 8px; flex-shrink: 0; }
.scaling-input, .scaling-select {
background: var(--bg-tertiary, #161b22); border: 1px solid var(--border-primary, #2d333b);
color: var(--text-primary, #e6edf3); border-radius: 6px; padding: 7px 12px;
font-size: 13px; outline: none; transition: border-color .15s;
}
.scaling-input:focus, .scaling-select:focus { border-color: var(--accent-blue, #58a6ff); }
.scaling-input::placeholder { color: var(--text-secondary, #8b949e); }
.scaling-hint { color: var(--text-secondary, #8b949e); font-size: 12px; margin: 0; line-height: 1.5; }
/* Empty state */
.scaling-empty {
text-align: center; color: var(--text-secondary, #8b949e);
padding: 40px 16px; font-size: 13px;
}
/* Tables */
.scaling-table-wrap { overflow-x: auto; }
.scaling-table {
width: 100%; border-collapse: collapse;
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 8px; overflow: hidden;
}
.scaling-table th {
text-align: left; padding: 10px 14px; font-size: 11px;
text-transform: uppercase; letter-spacing: .5px;
color: var(--text-secondary, #8b949e);
background: var(--bg-tertiary, #161b22);
border-bottom: 1px solid var(--border-primary, #2d333b);
white-space: nowrap;
}
.scaling-table td {
padding: 10px 14px; font-size: 13px;
border-bottom: 1px solid var(--border-primary, #2d333b);
color: var(--text-primary, #e6edf3);
}
.scaling-table tbody tr:hover { background: rgba(88,166,255,.04); }
.scaling-table tbody tr:last-child td { border-bottom: none; }
/* Section titles */
.scaling-section-title {
display: flex; align-items: center; gap: 8px;
font-size: 15px; font-weight: 600; margin: 24px 0 12px;
color: var(--text-primary, #e6edf3);
}
.scaling-section-title .material-icons { font-size: 20px; color: var(--accent-blue, #58a6ff); }
/* Summary cards grid */
.scaling-summary-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px; margin-bottom: 24px;
}
.scaling-card {
display: flex; align-items: center; gap: 14px;
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 10px; padding: 18px 20px;
transition: border-color .15s;
}
.scaling-card:hover { border-color: var(--accent-blue, #58a6ff); }
.scaling-card-icon {
width: 44px; height: 44px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
background: rgba(88,166,255,.1);
}
.scaling-card-icon .material-icons { font-size: 22px; color: var(--accent-blue, #58a6ff); }
.scaling-card-value { font-size: 22px; font-weight: 700; color: var(--text-primary, #e6edf3); }
.scaling-card-label { font-size: 12px; color: var(--text-secondary, #8b949e); margin-top: 2px; }
/* Architecture diagram */
.scaling-architecture {
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 10px; padding: 20px; margin-bottom: 20px;
}
.scaling-architecture h3 {
display: flex; align-items: center; gap: 8px;
font-size: 14px; font-weight: 600; margin: 0 0 16px;
color: var(--text-primary, #e6edf3);
}
.scaling-architecture h3 .material-icons { font-size: 18px; color: var(--accent-blue, #58a6ff); }
.arch-diagram {
min-height: 180px; display: flex; align-items: center; justify-content: center;
position: relative;
}
.arch-diagram svg { width: 100%; height: auto; }
.arch-empty { color: var(--text-secondary, #8b949e); font-size: 13px; }
/* Relay health grid */
.scaling-relay-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
}
.relay-health-card {
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 10px; padding: 16px; transition: border-color .15s;
}
.relay-health-card:hover { border-color: var(--accent-blue, #58a6ff); }
.relay-health-header {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
}
.relay-health-name {
font-size: 14px; font-weight: 600; color: var(--text-primary, #e6edf3);
display: flex; align-items: center; gap: 6px;
}
.relay-health-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.relay-health-dot.online { background: #3fb950; }
.relay-health-dot.degraded { background: #d29922; }
.relay-health-dot.offline { background: #f85149; }
.relay-health-location { font-size: 11px; color: var(--text-secondary, #8b949e); }
.relay-health-metrics {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.relay-metric {
display: flex; flex-direction: column; gap: 3px;
}
.relay-metric-label { font-size: 10px; text-transform: uppercase; color: var(--text-secondary, #8b949e); letter-spacing: .3px; }
.relay-metric-bar {
height: 6px; background: var(--bg-tertiary, #161b22);
border-radius: 3px; overflow: hidden;
}
.relay-metric-fill {
height: 100%; border-radius: 3px; transition: width .3s ease;
}
.relay-metric-fill.low { background: #3fb950; }
.relay-metric-fill.med { background: #d29922; }
.relay-metric-fill.high { background: #f85149; }
.relay-metric-value { font-size: 11px; color: var(--text-primary, #e6edf3); }
/* Capacity tiers */
.scaling-capacity-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;
}
.capacity-section {
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 10px; padding: 20px;
}
.capacity-section h3 {
display: flex; align-items: center; gap: 8px;
font-size: 14px; font-weight: 600; margin: 0 0 16px;
color: var(--text-primary, #e6edf3);
}
.capacity-section h3 .material-icons { font-size: 18px; color: var(--accent-blue, #58a6ff); }
.capacity-tier {
margin-bottom: 12px;
}
.capacity-tier-header {
display: flex; justify-content: space-between; margin-bottom: 4px;
}
.capacity-tier-name { font-size: 12px; font-weight: 600; color: var(--text-primary, #e6edf3); }
.capacity-tier-pct { font-size: 12px; color: var(--text-secondary, #8b949e); }
.capacity-tier-bar {
height: 8px; background: var(--bg-tertiary, #161b22);
border-radius: 4px; overflow: hidden;
}
.capacity-tier-fill {
height: 100%; border-radius: 4px; transition: width .4s ease;
}
.capacity-tier-fill.ok { background: #3fb950; }
.capacity-tier-fill.warn { background: #d29922; }
.capacity-tier-fill.crit { background: #f85149; }
/* Recommendations */
.capacity-recs { display: flex; flex-direction: column; gap: 8px; }
.capacity-rec {
display: flex; align-items: flex-start; gap: 8px;
padding: 10px 12px; border-radius: 6px;
font-size: 12px; line-height: 1.5;
}
.capacity-rec.info { background: rgba(88,166,255,.08); color: var(--text-primary, #e6edf3); }
.capacity-rec.warn { background: rgba(210,153,34,.08); color: #d29922; }
.capacity-rec.crit { background: rgba(248,81,73,.08); color: #f85149; }
.capacity-rec .material-icons { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
/* Status badges */
.scaling-status {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;
}
.scaling-status.online { background: rgba(63,185,80,.12); color: #3fb950; }
.scaling-status.degraded { background: rgba(210,153,34,.12); color: #d29922; }
.scaling-status.offline { background: rgba(248,81,73,.12); color: #f85149; }
.scaling-status.enabled { background: rgba(63,185,80,.12); color: #3fb950; }
.scaling-status.disabled { background: rgba(139,148,158,.12); color: #8b949e; }
/* Modals */
.modal-overlay {
position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.55); display: flex;
align-items: center; justify-content: center;
animation: fadeIn .15s ease;
}
.modal-card {
background: var(--bg-secondary, #0d1117);
border: 1px solid var(--border-primary, #2d333b);
border-radius: 12px; width: 460px; max-width: 95vw;
max-height: 85vh; overflow-y: auto;
}
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--border-primary, #2d333b);
}
.modal-header h3 { font-size: 15px; margin: 0; color: var(--text-primary, #e6edf3); }
.modal-close {
background: none; border: none; color: var(--text-secondary, #8b949e);
font-size: 20px; cursor: pointer; line-height: 1;
}
.modal-close:hover { color: var(--text-primary, #e6edf3); }
.modal-body { padding: 20px; }
.modal-body .form-group { margin-bottom: 14px; }
.modal-body .form-group label {
display: block; font-size: 12px; font-weight: 600;
color: var(--text-secondary, #8b949e); margin-bottom: 5px;
}
.modal-body .form-group input,
.modal-body .form-group select {
width: 100%; box-sizing: border-box;
}
.modal-footer {
display: flex; justify-content: flex-end; gap: 8px;
padding: 14px 20px; border-top: 1px solid var(--border-primary, #2d333b);
}
/* Buttons */
.btn { cursor: pointer; border: none; border-radius: 6px; font-weight: 600; transition: background .15s; }
.btn-sm { padding: 7px 14px; font-size: 12px; }
.btn-primary { background: var(--accent-blue, #58a6ff); color: #fff; }
.btn-primary:hover { background: #79b8ff; }
.btn-secondary { background: var(--bg-tertiary, #161b22); color: var(--text-primary, #e6edf3); border: 1px solid var(--border-primary, #2d333b); }
.btn-secondary:hover { background: var(--border-primary, #2d333b); }
.btn-danger { background: rgba(248,81,73,.15); color: #f85149; }
.btn-danger:hover { background: rgba(248,81,73,.25); }
.btn-icon { background: none; border: none; color: var(--text-secondary, #8b949e); cursor: pointer; padding: 4px; }
.btn-icon:hover { color: var(--text-primary, #e6edf3); }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* Responsive */
@media (max-width: 768px) {
.scaling-summary-grid { grid-template-columns: repeat(2, 1fr); }
.scaling-capacity-grid { grid-template-columns: 1fr; }
.scaling-relay-grid { grid-template-columns: 1fr; }
.scaling-tabs { overflow-x: auto; flex-wrap: nowrap; }
.scaling-tab span:not(.material-icons) { display: none; }
}
.scaling-info-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
margin-bottom: 12px;
background: rgba(96, 165, 250, 0.08);
border: 1px solid rgba(96, 165, 250, 0.25);
border-radius: 8px;
color: var(--text-secondary, #cbd5e1);
font-size: 13px;
line-height: 1.4;
}
.scaling-info-banner .material-icons {
color: #60a5fa;
font-size: 18px;
flex-shrink: 0;
}