diff --git a/src/web/src/cgi/auth_html.c b/src/web/src/cgi/auth_html.c index bdc78ed..173916c 100755 --- a/src/web/src/cgi/auth_html.c +++ b/src/web/src/cgi/auth_html.c @@ -18,6 +18,7 @@ /* 受保护的 HTML 页面列表 */ static const char *protected_pages[] = { + "overview.html", "status_info.html", "sys_set.html", "sys_rf.html", @@ -43,7 +44,7 @@ static const char* get_requested_page(const char *query_string) { static char page_name[256] = {0}; if (!query_string || strlen(query_string) == 0) { - return "status_info.html"; /* 默认页面 */ + return "overview.html"; /* 默认页面 */ } /* 解析 query string 中的 page=xxx 参数 */ @@ -57,7 +58,7 @@ static const char* get_requested_page(const char *query_string) { return page_name; } - return "status_info.html"; + return "overview.html"; } /* 发送重定向响应 */ @@ -130,7 +131,7 @@ int main(int argc, char **argv) { if (cookie && extract_cookie(cookie, SESSION_COOKIE_NAME, session_id, sizeof(session_id))) { if (validate_session(session_id, NULL, 0)) { - send_redirect("status_info.html"); + send_redirect("overview.html"); } } /* 否则返回登录页 */ diff --git a/src/web/src/cgi_login/cgi_main.c b/src/web/src/cgi_login/cgi_main.c index a2a1f10..a44881c 100644 --- a/src/web/src/cgi_login/cgi_main.c +++ b/src/web/src/cgi_login/cgi_main.c @@ -51,6 +51,29 @@ int simplified_create_client_socket(int port) return sockfd; } +/* Read an application value for a public login-page endpoint. */ +static int simplified_get_data_from_process(int modulePort, int moduleKey, char *buf, size_t bufLen) +{ + cgi_process_elem_t elem; + int fd; + + if(!buf || bufLen == 0 || modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE) + return -1; + memset(&elem, 0, sizeof(elem)); + elem.method = GET_METHOD; + elem.keyType = moduleKey; + fd = simplified_create_client_socket(modulePort); + if(fd < 0) + return -1; + if(write(fd, &elem, sizeof(elem)) != sizeof(elem) || read(fd, &elem, sizeof(elem)) != sizeof(elem)){ + close(fd); + return -1; + } + close(fd); + snprintf(buf, bufLen, "%s", elem.data); + return 0; +} + /* Define simplified version of set_data_from_process to avoid linking issues */ void simplified_set_data_from_process(int modulePort, int moduleKey, char *buf) { if(modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE) @@ -398,7 +421,7 @@ int main(int argc, char **argv) { sizeof(cookie_header), 0); printf("Content-type: text/html\r\n"); printf("%s\r\n", cookie_header); - printf("Location: status_info.html\r\n\r\n"); + printf("Location: overview.html\r\n\r\n"); exit(0); } } @@ -412,6 +435,16 @@ int main(int argc, char **argv) { /* === Handle GET request - get current username === */ if (method && strcasecmp(method, "GET") == 0 && query) { + /* Public read-only title used before a session exists on login.html. */ + if (strstr(query, "action=get_title")) { + char title[MSG_DATA_LEN] = {0}; + printf("Content-type: text/plain\r\nCache-Control: no-store\r\n\r\n"); + if(simplified_get_data_from_process(WEB_SOCKET_PORT, CGIKEY_WEB_TITLE, + title, sizeof(title)) == 0) + printf("%s", title); + exit(0); + } + /* Check for get_user action */ if (strstr(query, "action=get_user")) { char *cookie = getenv("HTTP_COOKIE"); diff --git a/src/web/src/cgi_login/cgi_main.h b/src/web/src/cgi_login/cgi_main.h index b485779..2817e89 100644 --- a/src/web/src/cgi_login/cgi_main.h +++ b/src/web/src/cgi_login/cgi_main.h @@ -19,7 +19,7 @@ #include #include -#define USER_WEB_INDEX_HTML "status_info.html" +#define USER_WEB_INDEX_HTML "overview.html" #endif diff --git a/src/web/www/css/styles.css b/src/web/www/css/styles.css index c3c6dcf..4e71619 100755 --- a/src/web/www/css/styles.css +++ b/src/web/www/css/styles.css @@ -7,7 +7,7 @@ --accent-green-bright: #4ade80; --accent-green-dim: #16a34a; /* Error / alert */ - --status-err: #ef4444; + --status-err: #ff5252; --status-err-dim: #dc2626; /* Warning / amber */ --status-warn: #f59e0b; @@ -23,15 +23,54 @@ /* Text */ --text-primary: #f9fafb; --text-secondary: #d1d5db; - --text-muted: #9ca3af; + --text-muted: #b0b8c4; + --plp-pill-bg: #3a4452; + --chart-inset: #2a313c; + /* Informational text/labels (status notes, progress labels). Light value set in .light-theme. */ + --info-text: var(--status-neutral); + /* Mode identity colours (overview Signal Chain page only). Light values set in .light-theme. + Gateway intentionally reuses --accent-green / --accent-green-bright (brand). */ + --mode-exciter: #60a5fa; --mode-exciter-hover: #93bbfd; + --mode-translator: #fb923c; --mode-translator-hover: #fdba74; + --mode-standalone: #a78bfa; --mode-standalone-hover: #c4b5fd; + --mode-exciter-tint: rgba(96,165,250,0.10); + --mode-translator-tint: rgba(251,146,60,0.10); + --mode-standalone-tint: rgba(167,139,250,0.10); + --mode-gateway-tint: rgba(34,197,94,0.10); + /* Stronger (hover) variants of the mode tints. */ + --mode-exciter-tint-strong: rgba(96,165,250,0.22); + --mode-translator-tint-strong: rgba(251,146,60,0.22); + --mode-standalone-tint-strong: rgba(167,139,250,0.22); + --mode-gateway-tint-strong: rgba(34,197,94,0.22); /* Borders */ --border-subtle: #4b5563; --border-accent: var(--accent-green); - /* Sidebar */ + /* Sidebar (DARK ISLAND — sidebar stays dark navy in BOTH themes, so these + tokens are NOT overridden in .light-theme; see the .light-theme block). */ --sidebar-bg: #0f172a; --sidebar-text: #94a3b8; --sidebar-text-active: #ffffff; --sidebar-hover: #1e293b; + /* Active-item accent. Lives as its own token (not --accent-green) because the + sidebar is a dark island: this MUST stay bright green in light theme, whereas + --accent-green darkens. Same value as --accent-green in dark theme. */ + --sidebar-accent: #22c55e; + /* Primary-action button family (theme-aware). Dark values = today's look, + so dark-only products are visually unchanged. Light values softened + (entry 011) so buttons sit in harmony on the light page, not as heavy + dark blocks. Status dots keep --accent-green and stay vivid. */ + --btn-green-bg: var(--accent-green-dim); + --btn-green-bg-hover: var(--accent-green); + --btn-green-text: #06230f; + --btn-green-text-hover: #06230f; + --btn-neutral-bg: #4b5563; + --btn-neutral-text: #ffffff; + /* Sidebar footer/labels: PINNED readable colours (sidebar is a dark island, + so these must NOT follow the theme tokens, which flip dark in light mode). */ + --sidebar-footer-h: #cbd5e1; + --sidebar-footer-p: #94a3b8; + --sidebar-footer-link: #7fb3d5; + --sidebar-label: #7c8595; /* Legacy / UI */ --primary: var(--accent-green); --primary-hover: var(--accent-green-dim); @@ -41,6 +80,31 @@ --sidebar-width: 220px; --header-height: 64px; --dashboard-height: auto; + + /* ===== PLOT PALETTE (family, added 2026-08-11) ============================== + Colours for the spectrum + eye canvases on the Measurements page. Present as + DORMANT family tokens: only smod renders a Measurements page today, but the + page is planned for other products, so the tokens live in the shared sheet + rather than being hand-copied later (the exact mistake that motivated this + whole stylesheet consolidation). Values are the instrument-plot palette; four + of them already equal existing family tokens (trace=--status-neutral, + hold=--status-warn, band=neutral@10%, eye-level near neutral) and are kept as + separate names so a family colour change cannot silently restyle a scope plot. + The plot field stays dark in BOTH themes on purpose: the eye is a density map + whose floor must read as "no signal", which a dark field gives and a light one + does not. Not overridden in .light-theme, deliberately. =================== */ + --plot-bg: #0b1220; + --plot-grid: #1e293b; + --plot-grid-zero: #334155; + --plot-axis-label: #94a3b8; + --plot-axis-title: #64748b; + --plot-trace: #38bdf8; + --plot-trace-hold: #f59e0b; + --plot-trace-error: #eab308; + --plot-marker: #f87171; + --plot-band: rgba(56, 189, 248, 0.10); + --plot-eye-level: rgba(57, 208, 255, 0.45); + --plot-eye-decision: #a3e635; } * { @@ -165,20 +229,41 @@ body { .app-sidebar li.active a { background: transparent; color: var(--sidebar-text-active); - border-left: 3px solid var(--accent-green); + border-left: 3px solid var(--sidebar-accent); font-weight: 600; } +/* --- Sidebar group labels (Operations / Configuration / Admin) --- + Added for ATSC 3.0 pages. Scoped by the .app-sidebar ancestor plus + the .ov-group-label class, so these rules only affect
  • elements + that opt in with class="ov-group-label". No existing pages in other + products (7x, 2D Marble, ATSC 1.0) use this class, so adding these + rules has zero visual or behavioral impact on them. + The existing .app-sidebar li a and .app-sidebar li.active a rules + require an child; these label items have no , so no collision. */ +.app-sidebar li.ov-group-label { + padding: 16px 16px 4px 16px; + font-size: 9px; + font-weight: 700; + letter-spacing: 1.6px; + text-transform: uppercase; + color: var(--sidebar-label); + border-left: 3px solid transparent; + pointer-events: none; + user-select: none; +} +.app-sidebar li.ov-group-label:first-child { padding-top: 6px; } + .sidebar-footer { padding: 16px; - color: var(--text-muted); + color: var(--sidebar-footer-p); font-size: 12px; line-height: 1.5; border-top: 1px solid var(--border-subtle); } .sidebar-footer h4 { - color: var(--text-secondary); + color: var(--sidebar-footer-h); margin-bottom: 8px; font-size: 13px; font-weight: 600; @@ -192,11 +277,11 @@ body { .sidebar-footer .redbox-label { font-weight: 600; - color: var(--text-secondary); + color: var(--sidebar-footer-h); } .sidebar-footer a { - color: var(--status-neutral); + color: var(--sidebar-footer-link); font-weight: 500; } @@ -537,8 +622,8 @@ body { font-size: 11px; font-weight: 700; letter-spacing: 0.5px; - background-color: var(--accent-green); - color: #fff; + background-color: var(--btn-green-bg); + color: var(--btn-green-text-hover); padding: 4px 10px; border-radius: 6px; margin-left: auto; @@ -547,7 +632,9 @@ body { } .box-header.collapsed .toggle-text { - background-color: var(--text-muted); + background-color: var(--btn-neutral-bg); + color: var(--btn-neutral-text); + border: 1px solid var(--border-subtle); } /* Accordion Specific Settings */ @@ -773,8 +860,8 @@ select.display-only-select:disabled.state-ok { /* Buttons */ input[type="button"].btn-modern, input[type="submit"].btn-modern { - background-color: var(--accent-green-dim); - color: var(--text-primary); + background-color: var(--btn-green-bg); + color: var(--btn-green-text); font-size: 13px; font-weight: 600; padding: 8px 18px; @@ -788,14 +875,14 @@ input[type="submit"].btn-modern { input[type="button"].btn-modern:hover, input[type="submit"].btn-modern:hover { - background-color: var(--accent-green); - border-color: var(--accent-green); - color: #fff; + background-color: var(--btn-green-bg-hover); + border-color: var(--btn-green-bg-hover); + color: var(--btn-green-text-hover); } input[type="button"].btn-modern:active, input[type="submit"].btn-modern:active { - background-color: var(--accent-green-dim); + background-color: var(--btn-green-bg); } input[type="button"].btn-modern:disabled, @@ -809,12 +896,26 @@ input[type="submit"].btn-modern:disabled { /* Unsaved reminder */ input[type="button"].btn-modern.is-unsaved:not(:disabled), input[type="submit"].btn-modern.is-unsaved:not(:disabled) { - background-color: var(--accent-green) !important; - border-color: var(--accent-green) !important; - color: #fff !important; + background-color: var(--btn-green-bg) !important; + border-color: var(--btn-green-bg-hover) !important; + color: var(--btn-green-text-hover) !important; box-shadow: 0 0 10px rgba(34, 197, 94, 0.35); } +/* Cancel / secondary modal button — neutral, theme-aware (replaces hardcoded #64748b). */ +input[type="button"].btn-modern.btn-cancel, +input[type="submit"].btn-modern.btn-cancel { + background-color: var(--btn-neutral-bg); + color: var(--btn-neutral-text); + border-color: var(--border-subtle); +} +input[type="button"].btn-modern.btn-cancel:hover, +input[type="submit"].btn-modern.btn-cancel:hover { + background-color: var(--btn-neutral-bg); + border-color: var(--text-muted); + color: var(--btn-neutral-text); +} + fieldset { border: none; display: flex; @@ -1073,9 +1174,9 @@ BUTTON VIEW DETAILS } .btn-view-details:hover { - background-color: var(--accent-green); - border-color: var(--accent-green); - color: #fff; + background-color: var(--btn-green-bg-hover); + border-color: var(--btn-green-bg-hover); + color: var(--btn-green-text-hover); } /* ========================================================================== @@ -1372,12 +1473,12 @@ BUTTON VIEW DETAILS /* PLP row layout — same style as subframe-header: label | bar | bitrate | pct */ .plp-row { - display: grid; - grid-template-columns: 42px 1fr auto auto; - gap: 2px 10px; - align-items: center; - padding: 6px 0; - border-bottom: 1px solid rgba(75, 85, 99, 0.4); +display: grid; +grid-template-columns: 56px 1fr auto auto auto; +gap: 2px 10px; +align-items: center; +padding: 6px 0; +border-bottom: 1px solid rgba(75, 85, 99, 0.4); } .plp-row:last-of-type { @@ -1395,7 +1496,7 @@ BUTTON VIEW DETAILS text-align: center; padding: 4px 0; border-radius: 4px; - background: rgba(75, 85, 99, 0.3); + background: var(--plp-pill-bg); } /* Bar track (background) */ @@ -1465,20 +1566,13 @@ BUTTON VIEW DETAILS .plp-pct-text.pct-red { color: var(--status-err); } .plp-pct-text.pct-muted { color: var(--text-muted); } -/* Detail line (services + FULL indicator, below bar) */ -.plp-detail { - grid-column: 2 / -1; - grid-row: 2; - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - +/* FULL indicator — beside pct text in the same row */ .plp-full-indicator { - font-size: 11px; - font-weight: 700; - color: var(--status-err); +grid-column: 5; +grid-row: 1; +font-size: 11px; +font-weight: 700; +color: var(--status-err); background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.35); padding: 1px 7px; @@ -1488,32 +1582,53 @@ BUTTON VIEW DETAILS } .plp-full-indicator.visible { - display: inline-block; - animation: alarmPulse 1.5s ease-in-out infinite; + display: inline-block; + animation: alarmPulse 1.5s ease-in-out infinite; } -.plp-services { - font-size: 13px; - font-weight: 600; - color: var(--status-neutral); - max-width: 400px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +/* PLP trend sparkline container — inline chart below each utilization bar. + JS renderer writes SVG into this container on each 2-sec poll. + Placed in grid row 2 (row 1 = bar + pct + FULL indicator) spanning the bar columns. + Services displayed as left sidebar panel inside the SVG when present. */ +.plp-trend-spark { +grid-column: 2 / -1; +grid-row: 2; +margin-top: 4px; +overflow: visible; } -.plp-services:empty { - display: none; +/* Hide trend container when PLP row is hidden */ +.plp-row[style*="display: none"] .plp-trend-spark, +.plp-row[style*="display:none"] .plp-trend-spark { + display: none; } -.plp-services::before { - content: '['; - color: var(--border-subtle); +/* Sparkline SVG element — fills container width. +ViewBox 700×120 with preserveAspectRatio="none" — stretches to match bar width. +When services exist, left margin widens (ML += svcBandW) to accommodate sidebar panel. */ +.plp-trend-svg { +display: block; +width: 100%; +/* height set dynamically by JS via inline style — matches viewBox ratio */ } -.plp-services::after { - content: ']'; - color: var(--border-subtle); +/* Max/min envelope band — semi-transparent green fill (set inline by JS). */ +.plp-trend-svg polygon { + stroke: none; +} + +/* Subframe header trend — matches bar height */ +.subframe-header .plp-trend-spark { +grid-column: 2 / -1; +grid-row: 2; +margin-top: 4px; +} + +/* Global header trend — extra tall for emphasis */ +.global-header .plp-trend-spark { +grid-column: 2 / -1; +grid-row: 2; +margin-top: 4px; } /* Subframe header bar (total payload at top) */ @@ -1589,22 +1704,16 @@ BUTTON VIEW DETAILS /* Responsive adjustments for utilization bars */ @media (max-width: 767px) { - .plp-row { - grid-template-columns: 36px 1fr auto; - } +.plp-row { +grid-template-columns: 48px 1fr auto auto; +} .plp-row .plp-bitrate-text { font-size: 11px; } .plp-pct-text { display: none; } - .plp-detail { - flex-wrap: wrap; - } - .plp-services { - max-width: none; - } - .subframe-header { +.subframe-header { grid-template-columns: 48px 1fr; } .subframe-header .plp-bitrate-text { @@ -1615,12 +1724,12 @@ BUTTON VIEW DETAILS .global-header { grid-template-columns: 48px 1fr; } - .global-header .plp-bitrate-text { - grid-column: 1 / -1; - text-align: center; - margin-top: 4px; - } -} +.global-header .plp-bitrate-text { + grid-column: 1 / -1; + text-align: center; + margin-top: 4px; + } + } /* ========================================================================== SESSION TIMEOUT WARNING @@ -1694,3 +1803,448 @@ SESSION TIMEOUT WARNING .session-timeout-actions .btn-modern { min-width: 120px; } + +/* ============================================================================= + Sprint 1 — dashboard bar mode-adaptive visibility + group separators + Additive only. No existing rule, selector, or variable is modified. + Shared with 2D and exciter UIs — these classes are new and will only apply + where the new data-tile attribute is present on summary-card elements. + ============================================================================= */ + +/* Marker class for tile-hidden state (reserved; current implementation uses + inline style="display:none" set by applyBarTileVisibility for idempotency. + Class kept for future use: allow CSS-only hiding via toggled class if we + move to a transition/animation approach later.) */ +.summary-card[data-tile].tile-hidden { display: none; } + +/* Group-separator spacing for the bar. + The bar is a flex row; group separation is a slightly larger left margin + on the first tile of each group (applied via a data-group-first attribute + that pages set on select tiles). + Purely additive. Tiles without data-group-first are unaffected. */ +.summary-card[data-group-first="input"] { margin-left: 8px; } +.summary-card[data-group-first="output"] { margin-left: 8px; } +.summary-card[data-group-first="housekeeping"] { margin-left: 8px; } + +/* ============================================================================= + Sprint 1 Increment 6 — Live Trends panel (Overview page only) + Additive only. No existing rule, selector, or variable is modified. + These classes are new (ov-trend-*); they only apply to the new panel + container on Overview and do not affect the shared 2D / exciter UI. + ============================================================================= */ + +/* Panel container — visual weight matches the existing ov-chain-card. */ +.ov-trend-panel { + background: var(--bg-panel); + border: 1px solid var(--border-subtle); + border-radius: 8px; + padding: 16px 20px; + margin: 16px 0; +} + +.ov-trend-panel:empty { display: none; } + +/* Header row — small caps, dot, title, buffer-age label. */ +.ov-trend-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + letter-spacing: 0.08em; + color: var(--text-muted); + margin-bottom: 12px; + text-transform: uppercase; +} + +.ov-trend-header .ov-trend-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--status-ok, #22c55e); + box-shadow: 0 0 4px rgba(34, 197, 94, 0.5); +} + +.ov-trend-header .ov-trend-title { + color: var(--text-primary); + font-weight: 600; +} + +.ov-trend-header .ov-trend-sep { + opacity: 0.5; +} + +.ov-trend-header .ov-trend-age { + color: var(--text-muted); +} + +/* Grid of per-metric cards. Uses flex so cards auto-size to available + width, wrapping to a new row only when needed. */ +.ov-trend-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.ov-trend-card { + flex: 1 1 200px; + min-width: 180px; + background: var(--bg-card); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: 10px 12px; +} + +.ov-trend-label { + font-size: 10px; + letter-spacing: 0.08em; + color: var(--text-muted); + margin-bottom: 4px; +} + +.ov-trend-value { + display: flex; + align-items: baseline; + gap: 4px; + margin-bottom: 8px; + white-space: nowrap; +} + +.ov-trend-num { + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.ov-trend-unit { + font-size: 10px; + color: var(--text-muted); + /* No text-transform: units carry meaningful case (Mbps / dB / dBm / + dBc / MHz / %). Matches the signal chain's .ov-blk-primary .ov-u + convention; avoids collapsing dB / dBm / dBc distinctions into the + same upper-cased suffix. */ + letter-spacing: 0.04em; +} + +/* The sparkline itself. Height fixed; width fills the card. */ +.ov-trend-spark { + display: block; + width: 100%; + height: 40px; + stroke: var(--status-ok, #22c55e); + stroke-width: 1.5; + fill: none; + stroke-linejoin: round; + stroke-linecap: round; +} + +/* N/A overlay wrapper -- anchors the overlay text above the sparkline. + Positioned so overlay sits centered on the sparkline area. */ +.ov-trend-spark-wrap { + position: relative; +} + +/* Shown on FSSI-affected charts (SNR, FWD%, UIMD, LIMD) when FSSI is + active. Per v0.4 B4: overlay reads "N/A -- FSSI active" and line + drawing is suppressed (handled by extractors returning null). + pointer-events: none so the overlay never intercepts clicks. */ +.ov-trend-na-overlay { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + letter-spacing: 0.04em; + color: var(--text-muted, #8a8a8a); + background: rgba(0, 0, 0, 0.04); + pointer-events: none; + text-transform: none; +} + +/* When FSSI-active, tone down the card's current-value number. */ +.ov-trend-card-na .ov-trend-num-na { + color: var(--text-muted, #8a8a8a); + font-weight: 500; +} + +/* Min/max labels overlaid on the sparkline's right edge. Show the raw + data range (not the minSpan-floored display range) so operators can + always read the actual values — important when minSpan stretches a + tiny real variation into a taller chart. pointer-events: none so the + labels never intercept interaction with the svg. */ +.ov-trend-spark-labels { + position: absolute; + top: 0; + right: 2px; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + pointer-events: none; + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 9px; + line-height: 1; + color: var(--text-secondary); +} + +.ov-trend-label-max, +.ov-trend-label-min { + /* Units are not repeated on these labels — the card's main value + already shows the unit, and repeating it here adds clutter. */ + padding: 1px 2px; + background: var(--bg-main); + border-radius: 2px; +} + +/* ========================================================================== + CONTEXTUAL WARNING NOTES (added 2026-05-31) + Shared classes for the amber "this feature is gated / mode-restricted" + messages that were previously hand-written inline on each page. + Two variants: + .warning-banner — block callout (tinted box) above a section + .field-note — small inline note appended after a field/label + IMPORTANT: these classes intentionally do NOT set `display`. Visibility is + toggled by page JS via `element.style.display = ''` (show) / 'none' (hide), + which relies on the element's own inline `display:none` default. Adding + `display` here would break that toggling (a cleared inline value would fall + through to this rule). Keep `style="display:none"` on the element. + ========================================================================== */ +.warning-banner { + padding: 8px 14px; + margin-bottom: 8px; + font-size: 12px; + color: var(--status-warn); + background: rgba(245, 158, 11, 0.08); + border: 1px solid rgba(245, 158, 11, 0.2); + border-radius: 4px; +} +.field-note { + font-size: 11px; + color: var(--status-warn); + margin-left: 8px; +} +/* Error state for .warning-banner — red instead of amber (e.g. expired cert). + Toggle `is-error` in JS instead of setting inline colors. */ +.warning-banner.is-error { + color: var(--status-err); + background: rgba(239, 68, 68, 0.08); + border-color: rgba(239, 68, 68, 0.2); +} + +/* ========================================================================== + SECONDARY (CANCEL) BUTTON (added 2026-05-31) + Neutral-grey companion to .btn-modern, for Cancel / dismiss actions (e.g. the + account-settings modal). Use: class="btn-modern btn-secondary" + It only overrides the colour of .btn-modern; size/shape/hover come from + .btn-modern. Theme-aware via --bg-card-hover. + ========================================================================== */ +input[type="button"].btn-secondary, +input[type="submit"].btn-secondary { + background-color: var(--bg-card-hover); +} +input[type="button"].btn-secondary:hover, +input[type="submit"].btn-secondary:hover { + background-color: var(--border-subtle); + border-color: var(--border-subtle); +} + +/* ========================================================================== + PROGRESS BARS (added 2026-05-31) + Restart / reset / firmware-update progress. The bar fill is styled via the + browser pseudo-elements (the plain `color` property does NOT colour a + fill in Chromium/WebKit). Fill colour matches the info-text notes + shown beside each bar. Theme-aware via --info-text. + ========================================================================== */ +progress.aw-progress { + width: 100%; + height: 24px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + border-radius: 4px; + overflow: hidden; + background: var(--bg-card); +} +progress.aw-progress::-webkit-progress-bar { background: var(--bg-card); border-radius: 4px; } +progress.aw-progress::-webkit-progress-value { background: var(--info-text); border-radius: 4px; } +progress.aw-progress::-moz-progress-bar { background: var(--info-text); } + +/* ============================================================================ + LIGHT THEME (entry 010 — additive, owner-approved palette "Concept C balanced") + ---------------------------------------------------------------------------- + Activated by adding class "light-theme" to the root element. Default + (no class) = dark theme above. This block ONLY overrides token VALUES; it adds + no new selectors that affect dark mode and changes nothing in :root. + + Design intent (owner-approved, comfort-contrast — AA floor, NOT maximum): + - Page is soft grey (#e8ebf0), NOT white — reduces glare/eye-fatigue. + - Text is dark slate (#2a3340), NOT pure black — softer on the eyes. + - Status colours are muted, not neon. All measured AA on their real surface. + + DARK ISLAND: the sidebar stays dark navy in BOTH themes, so --sidebar-bg/-text/ + -text-active/-hover and --sidebar-accent are deliberately NOT overridden here — + they inherit their :root (dark) values. --sidebar-accent stays bright green. + + CSS owns colour; JS owns identity (entry 008) — so the mode-* overrides below + recolour the overview wizard/chips automatically with ZERO JS change. + ============================================================================ */ +:root.light-theme { + /* Green - primary accent / status good / active */ + --accent-green: #0f7034; + --accent-green-bright: #126e30; + --accent-green-dim: #0c5a2a; + /* Error / alert */ + --status-err: #b5302a; + --status-err-dim: #8f231e; /* darker companion to --status-err */ + /* Warning / amber */ + --status-warn: #8f4a08; + --status-warn-dim: #7c4106; /* darker companion to --status-warn */ + /* Neutral / info */ + --status-neutral: #0a5f8f; + --status-neutral-dim: #084b70; /* darker companion to --status-neutral */ + /* Light palette surfaces */ + --bg-main: #dde2e9; /* soft grey page, NOT white */ + --bg-panel: #eaeef3; /* card surface, gentle off-white */ + --bg-card: #d8dee6; /* inset/chip surface */ + --bg-card-hover: #cbd2dc; + /* Text */ + --text-primary: #2a3340; /* dark slate, NOT pure black */ + --text-secondary: #3a4452; + --text-muted: #525b6a; + --plp-pill-bg: #cdd4de; + --chart-inset: #ccd3dd; + /* --info-text inherits the :root link to --status-neutral → resolves to #0a5f8f. + Not re-declared here on purpose (the link is theme-agnostic). */ + /* Mode identity colours (overview Signal Chain page). Gateway reuses --accent-green. */ + --mode-exciter: #15598f; --mode-exciter-hover: #1f6fb0; + --mode-translator: #9a420a; --mode-translator-hover: #c8590f; + --mode-standalone: #5b3da8; --mode-standalone-hover: #6f4dc6; + --mode-exciter-tint: rgba(21,89,143,0.10); + --mode-translator-tint: rgba(154,66,10,0.10); + --mode-standalone-tint: rgba(91,61,168,0.10); + --mode-gateway-tint: rgba(15,112,52,0.10); + --mode-exciter-tint-strong: rgba(21,89,143,0.22); + --mode-translator-tint-strong: rgba(154,66,10,0.22); + --mode-standalone-tint-strong: rgba(91,61,168,0.22); + --mode-gateway-tint-strong: rgba(15,112,52,0.22); + /* Light button family: soft green + dark-green text (Strategy 2); soft grey + neutral. These keep white-on-dark NOT used here — light uses dark text. */ + --btn-green-bg: #8fcfa8; + --btn-green-bg-hover: #7bc098; + --btn-green-text: #06230f; + --btn-green-text-hover: #06230f; + --btn-neutral-bg: #d4d9e1; + --btn-neutral-text: #3a4452; + /* NOTE: --sidebar-footer-*, --sidebar-label are intentionally NOT redefined + here — the sidebar is a dark island, so they keep their pinned :root values. */ + /* Borders */ + --border-subtle: #cdd3dc; + /* --border-accent inherits :root link to --accent-green → resolves to #0f7034. */ + /* Sidebar tokens: NOT overridden (dark island). --sidebar-accent stays #22c55e. */ + /* --primary / --primary-hover inherit :root links to --accent-green(-dim). */ +} + +/* ============================================================================ + THEME TOGGLE + UTILITY CLUSTER (entry 010 — additive, light-theme feature) + ---------------------------------------------------------------------------- + A header control to switch dark/light. Grouped with the Admin dropdown in a + "utility cluster" so the two read as one controls zone on the right edge. + Styling: faint green fill + green text/icon + green border (owner-approved + "Option 1+2"). Wording names the DESTINATION ("Light Mode"/"Dark Mode") with + a matching sun/moon icon. Collapses to icon-only at narrow widths. + + NOTE: the cluster must NOT set overflow:hidden — .dropdown-content is + position:absolute and would be clipped. Cluster is transparent/no-clip. + ============================================================================ */ + +/* Cluster wraps the toggle + the existing Admin dropdown

    contents. */ +/* The cluster is a visible "controls" panel: subtle card background, border, + rounded, slight lift — so the toggle + Admin read as ONE unit (owner-approved + "3a"). It does NOT clip (dropdown-content is absolute and must escape). */ +.utility-cluster { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--bg-card); + border: 1px solid var(--border-subtle); + border-radius: 9px; + padding: 4px 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18); +} +/* Thin divider between the toggle and the Admin dropdown inside the cluster. */ +.utility-divider { + width: 1px; + height: 22px; + background: var(--border-subtle); + flex-shrink: 0; +} +/* Inside the cluster, flatten the toggle + Admin so they read as cluster items, + not standalone buttons. They keep their colour but drop their own box. */ +.utility-cluster .theme-toggle { + background: transparent; + border: 1px solid transparent; + height: 30px; + padding: 0 10px; +} +.utility-cluster .theme-toggle:hover { + background: var(--mode-gateway-tint); + border-color: var(--accent-green); +} +.utility-cluster .dropbtn-modern { + background: transparent; + border: 1px solid transparent; +} +.utility-cluster .dropdown:hover .dropbtn-modern { + border-color: var(--accent-green); +} +/* The header

    already has margin-left:auto (pushes it to the right edge). + We keep that and just lay out the cluster inside it, right-aligned. */ +.app-header h3.has-toggle { display: flex; align-items: center; justify-content: flex-end; } + +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 7px; + height: 34px; + padding: 0 12px; + border-radius: 6px; + cursor: pointer; + font-family: inherit; + font-size: 13px; + font-weight: 600; + white-space: nowrap; + flex-shrink: 0; + background: var(--mode-gateway-tint); + border: 1px solid var(--accent-green); + color: var(--accent-green); + transition: background 0.15s ease, border-color 0.15s ease; +} +.theme-toggle:hover { background: var(--bg-card-hover); } +.theme-toggle svg { width: 16px; height: 16px; color: var(--accent-green); } + +/* Icon + label swap by theme. Default (dark) shows "Light Mode" + sun + (the destination); .light-theme shows "Dark Mode" + moon. */ +.theme-toggle .tt-sun { display: inline-flex; } +.theme-toggle .tt-moon { display: none; } +.theme-toggle .tt-to-light { display: inline; } +.theme-toggle .tt-to-dark { display: none; } +:root.light-theme .theme-toggle .tt-sun { display: none; } +:root.light-theme .theme-toggle .tt-moon { display: inline-flex; } +:root.light-theme .theme-toggle .tt-to-light { display: none; } +:root.light-theme .theme-toggle .tt-to-dark { display: inline; } + +/* Smooth theme transition on the big surfaces. */ +body, .app-header, .app-sidebar, .scard, .panel, .card { + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} + +/* Narrow widths: collapse the toggle to icon-only (drop the text label). */ +@media (max-width: 600px) { + .theme-toggle .tt-label { display: none; } + .theme-toggle { padding: 0 9px; } +} diff --git a/src/web/www/js/dashboard.js b/src/web/www/js/dashboard.js index b0c1e45..15efdef 100755 --- a/src/web/www/js/dashboard.js +++ b/src/web/www/js/dashboard.js @@ -14,8 +14,32 @@ // Power decimal lookup table var POWER_DECIMALS = ["00","05","10","15","20","25","30","35","40","45","50","55","60","65","70","75","80","85","90","95"]; - // Input source labels - var INPUT_SOURCES = ["ASI1","ASI2","EAS","Tuner","TSoIP","None"]; + // Input source labels — C1 (2026-08-05): smod's OWN status map. Index 5 = SMPTE per + // smod firmware (matches the status page's proven parse). Copied by LABEL from the + // family, values re-read from this product (register W9 — never copy values). + // SMPTE slot PROVISIONAL pending the board-path hardware answer (team question). + // "None" is NOT a source: it renders only for absent/out-of-range data via + // getInputSource(). Frozen so no page can patch it into a parallel list again. + var INPUT_SOURCES = Object.freeze(["ASI1","ASI2","EAS","Tuner","TSoIP","SMPTE"]); + + /** + * Map a firmware input-source index to its display name. + * The ONLY translator from form[1] to a label — status_info.html calls this too + * (C1 killed its private ternary). Out-of-range/absent -> "None" (genuine no-input). + */ + function getInputSource(index) { + var i = Number(index); + return (isFinite(i) && i >= 0 && i < INPUT_SOURCES.length) ? INPUT_SOURCES[i] : "None"; + } + + /** + * R-2 (2026-08-05): NaN guard for numeric card population (in-tree pattern from + * sys_spectrum.html). Finite -> formatted via fn; else "--" (readback convention). + */ + function fmtNum(v, fn) { + var n = Number(v); + return isFinite(n) ? fn(n) : "--"; + } /** * Calculate frequency from channel number @@ -42,6 +66,7 @@ * @returns {number|string} Temperature in Fahrenheit */ function convertTemperature(rawValue, unit) { + if (!isFinite(Number(rawValue))) return "--"; // R-2: absent data -> "--", never NaN if (unit === 0) { return rawValue; // Already Fahrenheit } else { @@ -74,21 +99,32 @@ * @returns {string} Status class */ function getSnrStatus(snr) { - if (snr === "N/A") return 'off'; - var val = parseFloat(snr); // already carries the +10 dB display offset - if (val >= 37) return 'ok'; - if (val >= 30) return 'warn'; + // "N/A" and "N/A-FSSI" both mean no meaningful reading -> grey, never red. + // (FSSI parity round; the isNaN belt matches the 1.0 core's fall-through.) + if (typeof snr === "string" && snr.indexOf("N/A") === 0) return 'off'; + var val = parseFloat(snr); // true measured SNR/MER in dB + if (isNaN(val)) return 'off'; + if (val >= 27) return 'ok'; + if (val >= 20) return 'warn'; return 'err'; } + function updateSpectrumFssi(aFssi, bFssi, abMode) { + var fssi = document.getElementById('rd_fssi'); + if (!fssi) return; + fssi.innerText = (abMode === 1) ? (aFssi + ' / ' + bFssi) : aFssi; + } + /** * Get IMD status (UIMD/LIMD) * @param {string} imd - IMD value or "N/A" * @returns {string} Status class */ function getImdStatus(imd) { - if (imd === "N/A") return 'off'; - return parseFloat(imd) >= 47 ? 'ok' : 'err'; + if (typeof imd === "string" && imd.indexOf("N/A") === 0) return 'off'; + var v = parseFloat(imd); + if (isNaN(v)) return 'off'; + return v >= 47 ? 'ok' : 'err'; } /** @@ -126,7 +162,10 @@ (data.ctrl.toUpperCase() === "REMOTE") ? 'neutral' : 'warn'); setCard('sum_rf', 'dot_rf', (data.rfStat === 1) ? "On" : "Off", (data.rfStat === 1) ? 'ok' : 'off'); - setCard('sum_outpow', 'dot_outpow', data.finalPower, 'ok'); + // REG-5 (2026-08-05, 1.0 C.4 parity): OUTPUT POWER must not stay green with a + // stale figure while RF is off/muted — grey it whenever RF is not On. + setCard('sum_outpow', 'dot_outpow', data.finalPower, + (data.rfStat === 1) ? 'ok' : 'off'); }; /** @@ -186,19 +225,25 @@ var chan = Number(form[7]); var freqVal = calcFrequency(chan); - // SNR: half-dB from the app plus the fixed +10 dB display offset. - var snr = (ab_modenum === 1) ? "N/A" : (Number(form[11]) / 2 + 10).toFixed(1); + // SNR: half-dB value from the app, displayed as the true measured SNR/MER. + var snr = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[11], function(n){ return (n / 2).toFixed(1); }); - // LIMD / UIMD: half-dB from the app plus the fixed +11 dB display offset. - // Rounding the sum and then appending the half misreports odd half-dB - // values by 1 dB, so format the sum directly. - var limd = (ab_modenum === 1) ? "N/A" : (Number(form[12]) / 2 + 11).toFixed(1); - var uimd = (ab_modenum === 1) ? "N/A" : (Number(form[13]) / 2 + 11).toFixed(1); + // LIMD / UIMD are half-dB values from the app. Apply the fixed +11 dB + // display offset used by Status and the LCD. + var limd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[12], function(n){ return (n / 2 + 11).toFixed(1); }); + var uimd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[13], function(n){ return (n / 2 + 11).toFixed(1); }); - // Input source - var inputIdx = Number(form[1]); - if (inputIdx < 0 || inputIdx > 5) inputIdx = 5; - var inputSrc = INPUT_SOURCES[inputIdx]; + // The status interface already carries the shared A/B FSSI values. + // Spectrum consumes the same values instead of issuing another request. + updateSpectrumFssi(Number(form[16]), Number(form[17]), ab_modenum); + window.fssiRefreshActive = (ab_modenum === 1); + if (typeof window.setSpectrumRefreshActive === 'function') { + window.setSpectrumRefreshActive(window.fssiRefreshActive); + } + + // Input source — C1: via the single accessor; the old >5 clamp is gone + // (it is what rendered firmware idx 5 = SMPTE as "None"). + var inputSrc = getInputSource(form[1]); // Input status var inputStatus = (Number(form[2]) === 0) ? "OK" : "Err"; @@ -222,7 +267,8 @@ var p_sign = (Number(rf_form[1]) === 0) ? "+" : "-"; var p_int = Number(rf_form[2]); var p_dec = POWER_DECIMALS[Number(rf_form[3])] || "00"; - var finalPower = p_sign + p_int + "." + p_dec; + // R-2: absent power data -> "--", never "+NaN.00" + var finalPower = isFinite(p_int) ? (p_sign + p_int + "." + p_dec) : "--"; // Update all cards using the shared function window.updateDashboardCards({ @@ -254,7 +300,8 @@ getSnrStatus: getSnrStatus, getImdStatus: getImdStatus, POWER_DECIMALS: POWER_DECIMALS, - INPUT_SOURCES: INPUT_SOURCES + INPUT_SOURCES: INPUT_SOURCES, + getInputSource: getInputSource }; })(); diff --git a/src/web/www/js/theme.js b/src/web/www/js/theme.js new file mode 100644 index 0000000..6f76aa7 --- /dev/null +++ b/src/web/www/js/theme.js @@ -0,0 +1,49 @@ +/* smod js/theme.js — ported verbatim from the 1.0 dashboard_core.js:605-651 (entry 010) + * as its own file because smod's login.html loads no dashboard.js and the module must run + * in on EVERY page (prevents a wrong-theme first paint). Theme round 2026-08-06. */ +/* ============================================================================ + * THEME TOGGLE (light/dark) — entry 010, light-theme feature + * ---------------------------------------------------------------------------- + * Adds/removes the "light-theme" class on the root element. CSS owns all + * colour (see styles.css :root.light-theme block); this only flips the class + * and remembers the choice. Default = dark (no class). Exposed as a global so + * the inline header onclick can reach it, matching this codebase's convention + * of page-local inline handlers (e.g. toggleSidebar). + * + * localStorage is used for persistence. This is real on-device firmware, where + * localStorage is available; the embedded browser retains it across reloads. + * ==========================================================================*/ +(function () { + 'use strict'; + var KEY = 'aw-theme'; // stored value: 'light' or 'dark' + var CLASS = 'light-theme'; + var root = document.documentElement; + + function apply(theme) { + if (theme === 'light') { + root.classList.add(CLASS); + } else { + root.classList.remove(CLASS); + } + } + + // Apply the saved choice as early as possible to avoid a flash of the + // wrong theme. dashboard_core.js is loaded in , so this runs before + // the body paints on most pages. + try { + var saved = window.localStorage ? localStorage.getItem(KEY) : null; + apply(saved === 'light' ? 'light' : 'dark'); // default dark + } catch (e) { + apply('dark'); + } + + // Global handler for the header toggle button's onclick. + window.toggleTheme = function () { + var nowLight = root.classList.toggle(CLASS); + try { + if (window.localStorage) { + localStorage.setItem(KEY, nowLight ? 'light' : 'dark'); + } + } catch (e) { /* storage unavailable; theme still applies for this session */ } + }; +})(); diff --git a/src/web/www/log_output.html b/src/web/www/log_output.html index 16775bb..ac40998 100755 --- a/src/web/www/log_output.html +++ b/src/web/www/log_output.html @@ -4,7 +4,8 @@ - + + @@ -299,12 +300,13 @@
    -
    -
    - - UIMD -
    -
    - -- - dBc -
    -
    @@ -391,6 +391,16 @@ dBc
    +
    +
    + + UIMD +
    +
    + -- + dBc +
    +
    diff --git a/src/web/www/login.html b/src/web/www/login.html index ddceb2d..573e7b4 100644 --- a/src/web/www/login.html +++ b/src/web/www/login.html @@ -3,7 +3,8 @@ - +Digital Exciter + + + +
    + + + + +
    + +

    +

    +

    +
    + + + +
    +

    +
    + + +
    + + +
    +
    Signal Chain
    +
    + + LOADING… +
    +
    + + Change Input in Settings → + +
    + + +
    +
    + + Live Signal Chain · polling 5s · click a block to configure + showing last-known values +
    +
    +
    + + +
    + + +
    +
    +
    System
    +
    Temperature—
    +
    System Error—
    +
    Control—
    +
    RF Switch—
    +
    GPS—
    +
    +
    +
    Configuration
    +
    ATSC1.0
    +
    Sub-Mode—
    +
    Input—
    +
    Output—
    +
    +
    +
    Identity
    +
    FPGA—
    +
    MCU—
    +
    License—
    +
    SN—
    +
    +
    + +
    +
    + + + + + + diff --git a/src/web/www/status_info.html b/src/web/www/status_info.html index 821cef3..57b498a 100755 --- a/src/web/www/status_info.html +++ b/src/web/www/status_info.html @@ -4,6 +4,7 @@ + @@ -41,7 +42,19 @@ function get_remote_local_ctrl() { var table = data.split("|"); // Logic for remote/local control if needed // g_remot_local_ctrl = Number(table[0].split("#")[5]); - document.title = table[0].split("#")[11]; + /* P-4 ROOT CAUSE + the better fix (2026-08-06). + What was here: document.title = table[0].split("#")[11]; — copied from the + 1.0 BY VALUE, index and all (the same copy-by-value class as C1's + INPUT_SOURCES). The 1.0's sys_cfg carries five NTP fields after ctl so its + title sits at index 11; smod's sys_cfg has no NTP fields there, so index 11 + is ab_mode — the FSSI Refresh switch. The tab was displaying the FSSI switch + state: "0" or "1", flipping as FSSI was toggled (owner, 2026-08-06). + Why the line is DELETED rather than re-indexed: this page already has a + correct, authoritative title source — the dedicated web_title section polled + in SetFormDefaults below (table[7]). Two writers for one value is the defect, + not the index; re-indexing would have left a second writer racing the first + (and is exactly what C1 removed on the input-source axis). One source of + truth: the web_title section. This function now only does what its name says. */ }); } @@ -51,6 +64,11 @@ function get_remote_local_ctrl() { } function SetFormDefaults() { + // R-2 (2026-08-05): NaN guards — in-tree pattern from sys_spectrum.html. Readbacks + // show "--" for absent data; finite values format as before. + function fmtScaled(v, scale, dp){ var n = Number(v); return isFinite(n) ? (n*scale).toFixed(dp) : "--"; } + function fmtRaw(v){ var n = Number(v); return isFinite(n) ? n : "--"; } + var queryData = "State_SysState=&tuner_status=&pac_status=&gps_status=&State_SysVer=&mac_sn_status=&sys_rf=&web_title=&" $.get("atsc_status_cfg.cgi?"+queryData, function(data, status) { var temp_unit=1; @@ -63,10 +81,23 @@ function SetFormDefaults() { var ab_modenum = Number(form[25]); temp_unit = Number(form[26]); - // Input Source Map - var i = Number(form[1]); - var s_str = (i==0)?"ASI1":(i==1)?"ASI2":(i==2)?"EAS":(i==3)?"Tuner":(i==4)?"TSoIP":(i==5)?"SMPTE":"None"; - cf.asi_input.value = s_str; + // Input Source — C1 (2026-08-05): the page-private ternary is DELETED; this and the + // dashboard cards read the ONE map via DashboardHelpers.getInputSource (the two lists + // disagreed at index 5 — SMPTE vs None — which is your "None for a selected input"). + cf.asi_input.value = window.DashboardHelpers.getInputSource(form[1]); + + /* REG-2 / ST.F11 port (2026-08-05, from the 1.0): auto-expand Tuner Status on + ENTERING tuner/translator mode — smod IS a translator, so this is its primary + mode. Same "changed" semantics as the 1.0's adaptation of 3.0's publishMode: + expand only on the transition INTO Tuner input (form[1]===3, same index here, + proven by this page's own map), tracked by _stTunerMode — NEVER on every poll, + so an operator's manual collapse is not fought. Box-headers exist by now + (initCollapsibleTables runs at DOMContentLoaded, before this callback). */ + (function(){ + var inTuner = (Number(form[1]) === 3); + if (inTuner && window._stTunerMode !== true) { autoExpandTunerStatus(); } + window._stTunerMode = inTuner; + })(); // Input Status cf.asi_status.value = (Number(form[2])==0) ? "OK" : "Err"; @@ -76,12 +107,15 @@ function SetFormDefaults() { cf.ts_rate.value = form[4]; // TS Rate - // Temperature - if(temp_unit==0){ + // Temperature — R-2: absent data -> "--", never NaN cascades + var t_raw = Number(form[5]); + if(!isFinite(t_raw)){ + cf.temp1.value = "--"; cf.temp1_c.value = "--"; + } else if(temp_unit==0){ cf.temp1.value = form[5]; - cf.temp1_c.value = ((cf.temp1.value - 32)/1.8).toFixed(2); + cf.temp1_c.value = ((t_raw - 32)/1.8).toFixed(2); } else { - cf.temp1.value = (form[5]*1.8+32).toFixed(2); + cf.temp1.value = (t_raw*1.8+32).toFixed(2); cf.temp1_c.value = form[5]; } @@ -102,27 +136,27 @@ function SetFormDefaults() { // Offset i = Number(form[8]); - s_str = (i==0) ? "+" : "-"; + var s_str = (i==0) ? "+" : "-"; // scratch var — its declaration moved here when C1 deleted the input-source ternary that used to declare it cf.hz1.value = s_str + form[9]; // FWD Power & SNR var rawFwd = Number(form[10]); - cf.fwd.value = (ab_modenum==1) ? "N/A" : (rawFwd + " %"); + cf.fwd.value = (ab_modenum==1) ? "N/A-FSSI" : (rawFwd + " %"); // SNR. Half-dB from the app, shown as-is (PR12 removed the old fixed // +10 dB display offset; this is the true measured MER). var tmpdata = Number(form[11]); var snrStr = (tmpdata/2).toFixed(1); - cf.tsnr.value = (ab_modenum==1) ? "N/A" : snrStr; + cf.tsnr.value = (ab_modenum==1) ? "N/A-FSSI" : snrStr; // IMD. Half-dB from the app, plus the fixed +11 dB display offset. // (Do not Math.round() the sum and then paste on the half: for an odd // half-dB value that rounds x.5 up and reports 1 dB too much.) tmpdata = Number(form[12]); - cf.l_imd.value = (ab_modenum==1) ? "N/A" : (tmpdata/2 + 11).toFixed(1); + cf.l_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1); tmpdata = Number(form[13]); - cf.u_imd.value = (ab_modenum==1) ? "N/A" : (tmpdata/2 + 11).toFixed(1); + cf.u_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1); // Control & Amp cf.f1ctrl.value = (Number(form[14])==0) ? "Remote" : "Local"; @@ -143,6 +177,13 @@ function SetFormDefaults() { cf.b_fssi.value = (ab_modenum==1) ? (Number(form[17]) + " %") : "N/A"; // AGC State + /* FSSI active alternates the RFinA feedback path AGC realizes on, + so AGC state (and its error codes) are not meaningful — show the + reason instead of a misleading adjustment value. Takes priority. + (Ported from the 1.0, status_info.html:239-243 — FSSI parity round.) */ + if(ab_modenum==1){ + s_str = "N/A-FSSI"; + } else { agc_err_code = Number(form[27]); var agc_adj = Number(form[28]); if(agc_err_code==1) s_str="Down Limit!"; @@ -154,6 +195,7 @@ function SetFormDefaults() { s_str="OK"; } } + } cf.agc_state.value = s_str; // --- 2. PARSE SYS_RF FOR DASHBOARD --- @@ -191,37 +233,40 @@ function SetFormDefaults() { // Form 1: Tuner var form1 = table[1].split("#"); var cf1 = document.getElementById("tuner"); - cf1.rx_freq.value = Number(form1[1]); - cf1.rssi.value = Number(form1[2]); - cf1.rsnr.value = Number(form1[3]); + cf1.rx_freq.value = fmtRaw(form1[1]); + cf1.rssi.value = fmtRaw(form1[2]); + cf1.rsnr.value = fmtRaw(form1[3]); cf1.lock.selectedIndex = Number(form1[4]); // Form 2: PAC Status var form2 = table[2].split("#"); var cf2 = document.getElementById("pac_status"); - cf2.v9v.value = (Number(form2[1])*0.1).toFixed(2); - cf2.v12v.value = (Number(form2[2])*0.1).toFixed(2); - cf2.v50v.value = (Number(form2[3])*0.1).toFixed(2); - cf2.pac_fwd.value = (Number(form2[4])*0.1).toFixed(2); - cf2.ref.value = (Number(form2[5])*0.1).toFixed(2); - cf2.vswr.value = (Number(form2[6])*0.1).toFixed(2); + cf2.v9v.value = fmtScaled(form2[1], 0.1, 2); + cf2.v12v.value = fmtScaled(form2[2], 0.1, 2); + cf2.v50v.value = fmtScaled(form2[3], 0.1, 2); + cf2.pac_fwd.value = fmtScaled(form2[4], 0.1, 2); + cf2.ref.value = fmtScaled(form2[5], 0.1, 2); + cf2.vswr.value = fmtScaled(form2[6], 0.1, 2); - // PAC Temp - if(temp_unit==0){ - cf2.temp.value = (Number(form2[7])*0.1).toFixed(2); + // PAC Temp — R-2 guarded + var pac_t = Number(form2[7]); + if(!isFinite(pac_t)){ + cf2.temp.value = "--"; cf2.temp_c.value = "--"; + } else if(temp_unit==0){ + cf2.temp.value = (pac_t*0.1).toFixed(2); cf2.temp_c.value = (cf2.temp.value < 32) ? 0 : ((cf2.temp.value - 32)/1.8).toFixed(2); } else { - cf2.temp.value = (Number(form2[7])*0.1*1.8+32).toFixed(2); - cf2.temp_c.value = (Number(form2[7])*0.1).toFixed(2); - } + cf2.temp.value = (pac_t*0.1*1.8+32).toFixed(2); + cf2.temp_c.value = (pac_t*0.1).toFixed(2); + } cf2.pain.value = Number(form2[8]); // This is PA_IN - cf2.c50v1.value = (Number(form2[9])*0.1).toFixed(2); - cf2.c50v2.value = (Number(form2[10])*0.1).toFixed(2); - cf2.c50v3.value = (Number(form2[11])*0.1).toFixed(2); - cf2.c50v4.value = (Number(form2[12])*0.1).toFixed(2); - cf2.gv.value = (Number(form2[13])*0.1).toFixed(2); + cf2.c50v1.value = fmtScaled(form2[9], 0.1, 2); + cf2.c50v2.value = fmtScaled(form2[10], 0.1, 2); + cf2.c50v3.value = fmtScaled(form2[11], 0.1, 2); + cf2.c50v4.value = fmtScaled(form2[12], 0.1, 2); + cf2.gv.value = fmtScaled(form2[13], 0.1, 2); cf2.lvl.selectedIndex = Number(form2[14]); // Form 3: GPS @@ -252,8 +297,11 @@ function SetFormDefaults() { if (table[7]) { var title = table[7].split("#"); var titleStr = (title[1] != null) ? title[1] : ""; - document.getElementById('webtitle').textContent = titleStr; - document.getElementById('webtitle_header').textContent = titleStr; + /* P-4 guard: never paint a short/absent read over a good title (R-2 class). */ + if (titleStr !== "") { + document.getElementById('webtitle').textContent = titleStr; + document.getElementById('webtitle_header').textContent = titleStr; + } } // Update Status Colors and UI helpers @@ -330,14 +378,15 @@ function highlightProblemTableRows() { } // SNR, LIMD, UIMD: same error thresholds as summary cards / updateStatusColors var id = inp.id || ''; - if (id === 'tsnr' && v !== '' && v !== 'N/A') { + /* v.indexOf('N/A') !== 0 excludes both "N/A" and "N/A-FSSI" (FSSI-active) — 1.0 :413 pattern */ + if (id === 'tsnr' && v !== '' && v.indexOf('N/A') !== 0) { var snrNum = parseFloat(v); // true measured dB (PR12: no display offset) if (snrNum < 15) { tr.classList.add('row-has-error'); break; } } - if (id === 'l_imd' && v !== '' && v !== 'N/A') { + if (id === 'l_imd' && v !== '' && v.indexOf('N/A') !== 0) { if (parseFloat(v) < 47) { tr.classList.add('row-has-error'); break; } } - if (id === 'u_imd' && v !== '' && v !== 'N/A') { + if (id === 'u_imd' && v !== '' && v.indexOf('N/A') !== 0) { if (parseFloat(v) < 47) { tr.classList.add('row-has-error'); break; } } // License fields: INACTIVE is an error @@ -381,7 +430,7 @@ function updateStatusColors() { // Main Status details: LIMD, UIMD, SNR – same rules as summary cards (green/red/warn by range) var tsnrEl = document.getElementById("tsnr"); - if(tsnrEl && tsnrEl.value !== "" && tsnrEl.value !== "N/A") { + if(tsnrEl && tsnrEl.value !== "" && tsnrEl.value.indexOf("N/A") !== 0) { var snrVal = parseFloat(tsnrEl.value); // true measured dB (PR12: no display offset) if(snrVal >= 27) tsnrEl.style.color = color_ok; else if(snrVal >= 20) tsnrEl.style.color = color_warn; @@ -389,12 +438,12 @@ function updateStatusColors() { } else if(tsnrEl) tsnrEl.style.color = ""; var limdEl = document.getElementById("l_imd"); - if(limdEl && limdEl.value !== "" && limdEl.value !== "N/A") { + if(limdEl && limdEl.value !== "" && limdEl.value.indexOf("N/A") !== 0) { limdEl.style.color = parseFloat(limdEl.value) >= 47 ? color_ok : color_err; } else if(limdEl) limdEl.style.color = ""; var uimdEl = document.getElementById("u_imd"); - if(uimdEl && uimdEl.value !== "" && uimdEl.value !== "N/A") { + if(uimdEl && uimdEl.value !== "" && uimdEl.value.indexOf("N/A") !== 0) { uimdEl.style.color = parseFloat(uimdEl.value) >= 47 ? color_ok : color_err; } else if(uimdEl) uimdEl.style.color = ""; @@ -599,7 +648,28 @@ document.addEventListener('DOMContentLoaded', function() { } }); -// Collapsible Tables - MAIN STATUS, PAC STATUS, GPS STATUS +/* ST.F11 support (2026-08-05): expand the Tuner Status box if collapsed. Called only on + the transition into tuner/translator mode by the data callback — see the port note there. */ +function autoExpandTunerStatus() { + var headers = document.querySelectorAll('.box-header'); + for (var i = 0; i < headers.length; i++) { + var titleSpan = headers[i].querySelector('span'); + if (titleSpan && titleSpan.innerText.indexOf('Tuner Status') !== -1) { + if (headers[i].classList.contains('collapsed')) { + headers[i].classList.remove('collapsed'); + var content = headers[i].nextElementSibling; + if (content && content.className.indexOf('box-content') !== -1) { + content.style.display = 'block'; + } + var badge = headers[i].querySelector('.toggle-text'); + if (badge) badge.innerText = 'Hide Details'; + } + break; + } + } +} + +// Collapsible Tables - MAIN STATUS, TUNER STATUS, PAC STATUS, GPS STATUS function initCollapsibleTables() { var headers = document.querySelectorAll('.app-main h2'); for (var i = 0; i < headers.length; i++) { @@ -621,20 +691,26 @@ function initCollapsibleTables() { } if (box) { + /* REG-1 (2026-08-05, 1.0 C.2 port — "3.0 parity"): the primary status block + is OPEN on load so the operator sees it without a click; every other + section starts collapsed. Tuner Status is additionally auto-expanded on + entering translator mode by the data callback (ST.F11) — it cannot be + decided here because the mode is not yet read at init. */ + var startOpen = (headerText === "Main Status"); var boxHeader = document.createElement('div'); - boxHeader.className = "box-header collapsed"; + boxHeader.className = startOpen ? "box-header" : "box-header collapsed"; var titleSpan = document.createElement('span'); titleSpan.innerText = headerText + " Details"; var toggleSpan = document.createElement('span'); toggleSpan.className = "toggle-text"; - toggleSpan.innerText = "View Details"; + toggleSpan.innerText = startOpen ? "Hide Details" : "View Details"; boxHeader.appendChild(titleSpan); boxHeader.appendChild(toggleSpan); var contentDiv = document.createElement('div'); contentDiv.className = "box-content"; - contentDiv.style.display = "none"; + contentDiv.style.display = startOpen ? "block" : "none"; while (box.firstChild) { contentDiv.appendChild(box.firstChild); @@ -683,17 +759,17 @@ document.addEventListener('DOMContentLoaded', function() { -
    -
    -
    - - UIMD -
    -
    - -- - dBc -
    -
    @@ -780,6 +854,16 @@ document.addEventListener('DOMContentLoaded', function() { dBc
    +
    +
    + + UIMD +
    +
    + -- + dBc +
    +
    diff --git a/src/web/www/sys_psip.html b/src/web/www/sys_psip.html index 3f490ef..c2c00fe 100755 --- a/src/web/www/sys_psip.html +++ b/src/web/www/sys_psip.html @@ -4,7 +4,8 @@ - + + @@ -57,10 +58,99 @@ var table = data.split("|"); g_remot_local_ctrl = Number(table[0].split("#")[5]); - document.title = table[0].split("#")[11]; + /* P-4b (2026-08-06): the title write here — document.title = + table[0].split("#")[11] — is DELETED. On smod index 11 is ab_mode + (the FSSI Refresh switch), not the web title. This page already sets + the title from its web_title section below. Round 3 fixed only + status_info; this is the rest of that class. */ }); } +/* REG-4 (2026-08-05) — CTRL gate, ported from the 1.0 (its C.5, 3.0 SLT.7 parity; the + owner-found DESTRUCTIVE class: saving TSID/program rows while CTRL != Enable writes + non-effective values the operator believes took). When CTRL (ts_edit) != Enable(1), + the TSID save and every program-row save are disabled. The CTRL row's own Set + (#ts_ctrl_set) stays live so CTRL can be changed (CTRL+TSID share one post here, as + on the 1.0 — changing CTRL via that button is the intended path). No-op in Local + mode, where SetFormDefaults has already disabled everything. Safe on load and on + every ts_edit change. */ +/* P-6 (2026-08-06, CORRECTED after owner review). What the licence gate must do — + matched to what the 1.0 (sys_psip.html:157-168) and the 3.0 actually do, and to the + owner's ruling "no licence should STILL ALLOW Clear": + + no PSIP licence -> remove ONLY the "Enable" choice from ts_edit. + Disable(0) and Clear(2) stay fully usable; every Set stays live; NO grey-out, + NO banner. The operator can still disable or clear PSIP without a licence; they + simply cannot ENABLE it. + + The prior version of this function was the reverse of that — it forced the value off + Enable, disabled both Sets and the whole program table, and showed a banner. That + locked out Clear, which is exactly backwards. Corrected here. + + R4 (2026-08-11) — THE PREVIOUS COMMENT HERE WAS WRONG AND IS CORRECTED. + It claimed the 1.0's $('option').hide() "browsers do not honour reliably", promoted + from an ambiguous field report. Retrieval settled it: the 1.0's TSoIP page has NO + licence handling at all (zero references in sys_tsoip.html), so the option the owner + saw selectable cannot have been a 1.0 TSoIP gate; and smod had no gate whatsoever + before round 3, which explains "selectable on both" on its own. The owner then + confirmed the 1.0's Enable option DISAPPEARS when unlicensed — i.e. hiding works. + Owner ruling 2026-08-11: the option must be HIDDEN, not shown-but-unselectable. + + So the gate now hides AND disables: hidden removes it from the list (1.0 parity, and + what the owner sees), disabled is the belt for any engine that renders a hidden + option anyway. Disable(0) and Clear(2) and every Set remain untouched. */ +var g_psip_licensed = null; // null = not yet known; never gate on an unknown +function applyLicenceGate(){ + var edit = document.getElementById("ts_edit"); + if (!edit) return; + var enableOpt = edit.querySelector('option[value="1"]'); + if (!enableOpt) return; + if (g_psip_licensed === false) { + enableOpt.hidden = true; // out of the list entirely (owner ruling) + enableOpt.style.display = "none"; // same effect for engines ignoring [hidden] + enableOpt.disabled = true; // belt: unselectable even if rendered + if (Number(edit.value) === 1) { edit.value = "0"; } // if it was on Enable, fall back to Disable + } else { + enableOpt.hidden = false; // licensed or unknown -> Enable available + enableOpt.style.display = ""; + enableOpt.disabled = false; + } + // Disable/Clear and every Set are deliberately left ALONE — Clear must work unlicensed. +} +function get_licence(){ + $.get("atsc_status_cfg.cgi?sn_license=&", function(data, status){ + if (status !== "success" || !data) return; // unknown -> no gate + var licence_para = data.split("|")[0].split("#"); + var raw = licence_para[2]; + var key_psip = Number(raw); + /* Fail-open on a garbled read (round-1 bench caught the fail-closed version): only a + clean 0/1 is a licence answer; anything else stays unknown and ungated. */ + if (raw === undefined || raw === "" || isNaN(key_psip)) return; + g_psip_licensed = (key_psip === 1); + applyLicenceGate(); + }); +} + +function applyCtrlGate() { + if (g_remot_local_ctrl == 1) return; // Local: all already disabled + var edit = document.getElementById("ts_edit"); + var enabled = edit && Number(edit.value) === 1; // 1 = Enable + if (enabled) return; // Enable: normal Set state stands + // Not Enable -> block dependent saves; leave #ts_ctrl_set live. + var tsidBtn = document.getElementById("ts_tsid_set"); + if (tsidBtn) { tsidBtn.disabled = true; tsidBtn.value = "Set"; } + $('#psip_table_form input[type="button"]').prop("disabled", true).val("Set"); +} +// REG-4: re-gate the moment the operator changes CTRL in the form (registered once) +document.addEventListener('DOMContentLoaded', function () { + var tsEditEl = document.getElementById('ts_edit'); + if (tsEditEl) tsEditEl.addEventListener('change', function () { + applyCtrlGate(); + applyLicenceGate(); // P-6: licence outranks CTRL — re-assert on every change + }); + get_licence(); // P-6: read the licence once, gate when it answers +}); + // Updated SetFormDefaults to populate the Single Form function SetFormDefaults() { // 1. DISABLE BUTTONS START (only in Remote mode) @@ -133,6 +223,9 @@ function SetFormDefaults() { } if (typeof setTsidFormSnapshotUpdate === 'function') setTsidFormSnapshotUpdate(); if (typeof psipFormSnapshotUpdate === 'function') psipFormSnapshotUpdate(); + // REG-4: gate dependent saves whenever fresh CTRL state lands (after the re-enable above) + applyCtrlGate(); + applyLicenceGate(); // P-6: licence gate outranks CTRL gate; re-assert after every repaint document.getElementById('webtitle').textContent = title[1]; document.getElementById('webtitle_header').textContent = title[1]; @@ -474,12 +567,13 @@ function cmd_confirm() {
    -
    -
    - - UIMD -
    -
    - -- - dBc -
    -
    @@ -566,6 +658,16 @@ function cmd_confirm() { dBc
    +
    +
    + + UIMD +
    +
    + -- + dBc +
    +
    @@ -659,7 +761,7 @@ function cmd_confirm() { - + @@ -668,7 +770,7 @@ function cmd_confirm() { - + diff --git a/src/web/www/sys_rf.html b/src/web/www/sys_rf.html index 9709e8a..474fede 100755 --- a/src/web/www/sys_rf.html +++ b/src/web/www/sys_rf.html @@ -4,6 +4,7 @@ + @@ -66,7 +67,11 @@ function get_remote_local_ctrl() { var table = data.split("|"); g_remot_local_ctrl = Number(table[0].split("#")[5]); - document.title = table[0].split("#")[11]; + /* P-4b (2026-08-06): the title write here — document.title = + table[0].split("#")[11] — is DELETED. On smod index 11 is ab_mode + (the FSSI Refresh switch), not the web title. This page already sets + the title from its web_title section below. Round 3 fixed only + status_info; this is the rest of that class. */ }); } @@ -466,12 +471,13 @@ if (document.readyState === 'loading') {
    -
    -
    - - UIMD -
    -
    - -- - dBc -
    -
    @@ -558,6 +562,16 @@ if (document.readyState === 'loading') { dBc
    +
    +
    + + UIMD +
    +
    + -- + dBc +
    +
    @@ -696,6 +710,21 @@ if (document.readyState === 'loading') { + AGC @@ -882,7 +911,30 @@ if (document.readyState === 'loading') { + + +
    + Warning: Exciter RF Output Power will be reset to -25 dBm when the channel (TX Frequency) is changed. +
    +
    +

    Reference Output

    +
    @@ -897,21 +949,35 @@ if (document.readyState === 'loading') {
    10 MHz Output
    -
    - Enables the AB5 10 MHz reference clock output. Default Off; resets to Off on every power-up. + + +
    + Note: The 10 MHz reference clock output is Off by default, and resets to Off on every power-up.
    +
    -
    - Warning: Exciter RF Output Power will be reset to -25dBm when the channel(TX Frequency) is changed. -
    -