新增:概览仪表盘及统一频谱页面

This commit is contained in:
2026-08-28 13:27:39 +08:00
parent bded7b6836
commit 8243380474
14 changed files with 3262 additions and 333 deletions
+4 -3
View File
@@ -18,6 +18,7 @@
/* 受保护的 HTML 页面列表 */ /* 受保护的 HTML 页面列表 */
static const char *protected_pages[] = { static const char *protected_pages[] = {
"overview.html",
"status_info.html", "status_info.html",
"sys_set.html", "sys_set.html",
"sys_rf.html", "sys_rf.html",
@@ -43,7 +44,7 @@ static const char* get_requested_page(const char *query_string) {
static char page_name[256] = {0}; static char page_name[256] = {0};
if (!query_string || strlen(query_string) == 0) { if (!query_string || strlen(query_string) == 0) {
return "status_info.html"; /* 默认页面 */ return "overview.html"; /* 默认页面 */
} }
/* 解析 query string 中的 page=xxx 参数 */ /* 解析 query string 中的 page=xxx 参数 */
@@ -57,7 +58,7 @@ static const char* get_requested_page(const char *query_string) {
return page_name; 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, if (cookie && extract_cookie(cookie, SESSION_COOKIE_NAME,
session_id, sizeof(session_id))) { session_id, sizeof(session_id))) {
if (validate_session(session_id, NULL, 0)) { if (validate_session(session_id, NULL, 0)) {
send_redirect("status_info.html"); send_redirect("overview.html");
} }
} }
/* 否则返回登录页 */ /* 否则返回登录页 */
+34 -1
View File
@@ -51,6 +51,29 @@ int simplified_create_client_socket(int port)
return sockfd; 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 */ /* Define simplified version of set_data_from_process to avoid linking issues */
void simplified_set_data_from_process(int modulePort, int moduleKey, char *buf) { void simplified_set_data_from_process(int modulePort, int moduleKey, char *buf) {
if(modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE) if(modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE)
@@ -398,7 +421,7 @@ int main(int argc, char **argv) {
sizeof(cookie_header), 0); sizeof(cookie_header), 0);
printf("Content-type: text/html\r\n"); printf("Content-type: text/html\r\n");
printf("%s\r\n", cookie_header); 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); exit(0);
} }
} }
@@ -412,6 +435,16 @@ int main(int argc, char **argv) {
/* === Handle GET request - get current username === */ /* === Handle GET request - get current username === */
if (method && strcasecmp(method, "GET") == 0 && query) { 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 */ /* Check for get_user action */
if (strstr(query, "action=get_user")) { if (strstr(query, "action=get_user")) {
char *cookie = getenv("HTTP_COOKIE"); char *cookie = getenv("HTTP_COOKIE");
+1 -1
View File
@@ -19,7 +19,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <stdarg.h> #include <stdarg.h>
#define USER_WEB_INDEX_HTML "status_info.html" #define USER_WEB_INDEX_HTML "overview.html"
#endif #endif
+624 -70
View File
@@ -7,7 +7,7 @@
--accent-green-bright: #4ade80; --accent-green-bright: #4ade80;
--accent-green-dim: #16a34a; --accent-green-dim: #16a34a;
/* Error / alert */ /* Error / alert */
--status-err: #ef4444; --status-err: #ff5252;
--status-err-dim: #dc2626; --status-err-dim: #dc2626;
/* Warning / amber */ /* Warning / amber */
--status-warn: #f59e0b; --status-warn: #f59e0b;
@@ -23,15 +23,54 @@
/* Text */ /* Text */
--text-primary: #f9fafb; --text-primary: #f9fafb;
--text-secondary: #d1d5db; --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 */ /* Borders */
--border-subtle: #4b5563; --border-subtle: #4b5563;
--border-accent: var(--accent-green); --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-bg: #0f172a;
--sidebar-text: #94a3b8; --sidebar-text: #94a3b8;
--sidebar-text-active: #ffffff; --sidebar-text-active: #ffffff;
--sidebar-hover: #1e293b; --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 */ /* Legacy / UI */
--primary: var(--accent-green); --primary: var(--accent-green);
--primary-hover: var(--accent-green-dim); --primary-hover: var(--accent-green-dim);
@@ -41,6 +80,31 @@
--sidebar-width: 220px; --sidebar-width: 220px;
--header-height: 64px; --header-height: 64px;
--dashboard-height: auto; --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 { .app-sidebar li.active a {
background: transparent; background: transparent;
color: var(--sidebar-text-active); color: var(--sidebar-text-active);
border-left: 3px solid var(--accent-green); border-left: 3px solid var(--sidebar-accent);
font-weight: 600; 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 <li> 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 <a> child; these label items have no <a>, 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 { .sidebar-footer {
padding: 16px; padding: 16px;
color: var(--text-muted); color: var(--sidebar-footer-p);
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
} }
.sidebar-footer h4 { .sidebar-footer h4 {
color: var(--text-secondary); color: var(--sidebar-footer-h);
margin-bottom: 8px; margin-bottom: 8px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
@@ -192,11 +277,11 @@ body {
.sidebar-footer .redbox-label { .sidebar-footer .redbox-label {
font-weight: 600; font-weight: 600;
color: var(--text-secondary); color: var(--sidebar-footer-h);
} }
.sidebar-footer a { .sidebar-footer a {
color: var(--status-neutral); color: var(--sidebar-footer-link);
font-weight: 500; font-weight: 500;
} }
@@ -537,8 +622,8 @@ body {
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.5px; letter-spacing: 0.5px;
background-color: var(--accent-green); background-color: var(--btn-green-bg);
color: #fff; color: var(--btn-green-text-hover);
padding: 4px 10px; padding: 4px 10px;
border-radius: 6px; border-radius: 6px;
margin-left: auto; margin-left: auto;
@@ -547,7 +632,9 @@ body {
} }
.box-header.collapsed .toggle-text { .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 */ /* Accordion Specific Settings */
@@ -773,8 +860,8 @@ select.display-only-select:disabled.state-ok {
/* Buttons */ /* Buttons */
input[type="button"].btn-modern, input[type="button"].btn-modern,
input[type="submit"].btn-modern { input[type="submit"].btn-modern {
background-color: var(--accent-green-dim); background-color: var(--btn-green-bg);
color: var(--text-primary); color: var(--btn-green-text);
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
padding: 8px 18px; padding: 8px 18px;
@@ -788,14 +875,14 @@ input[type="submit"].btn-modern {
input[type="button"].btn-modern:hover, input[type="button"].btn-modern:hover,
input[type="submit"].btn-modern:hover { input[type="submit"].btn-modern:hover {
background-color: var(--accent-green); background-color: var(--btn-green-bg-hover);
border-color: var(--accent-green); border-color: var(--btn-green-bg-hover);
color: #fff; color: var(--btn-green-text-hover);
} }
input[type="button"].btn-modern:active, input[type="button"].btn-modern:active,
input[type="submit"].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, input[type="button"].btn-modern:disabled,
@@ -809,12 +896,26 @@ input[type="submit"].btn-modern:disabled {
/* Unsaved reminder */ /* Unsaved reminder */
input[type="button"].btn-modern.is-unsaved:not(:disabled), input[type="button"].btn-modern.is-unsaved:not(:disabled),
input[type="submit"].btn-modern.is-unsaved:not(:disabled) { input[type="submit"].btn-modern.is-unsaved:not(:disabled) {
background-color: var(--accent-green) !important; background-color: var(--btn-green-bg) !important;
border-color: var(--accent-green) !important; border-color: var(--btn-green-bg-hover) !important;
color: #fff !important; color: var(--btn-green-text-hover) !important;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.35); 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 { fieldset {
border: none; border: none;
display: flex; display: flex;
@@ -1073,9 +1174,9 @@ BUTTON VIEW DETAILS
} }
.btn-view-details:hover { .btn-view-details:hover {
background-color: var(--accent-green); background-color: var(--btn-green-bg-hover);
border-color: var(--accent-green); border-color: var(--btn-green-bg-hover);
color: #fff; 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 layout — same style as subframe-header: label | bar | bitrate | pct */
.plp-row { .plp-row {
display: grid; display: grid;
grid-template-columns: 42px 1fr auto auto; grid-template-columns: 56px 1fr auto auto auto;
gap: 2px 10px; gap: 2px 10px;
align-items: center; align-items: center;
padding: 6px 0; padding: 6px 0;
border-bottom: 1px solid rgba(75, 85, 99, 0.4); border-bottom: 1px solid rgba(75, 85, 99, 0.4);
} }
.plp-row:last-of-type { .plp-row:last-of-type {
@@ -1395,7 +1496,7 @@ BUTTON VIEW DETAILS
text-align: center; text-align: center;
padding: 4px 0; padding: 4px 0;
border-radius: 4px; border-radius: 4px;
background: rgba(75, 85, 99, 0.3); background: var(--plp-pill-bg);
} }
/* Bar track (background) */ /* Bar track (background) */
@@ -1465,20 +1566,13 @@ BUTTON VIEW DETAILS
.plp-pct-text.pct-red { color: var(--status-err); } .plp-pct-text.pct-red { color: var(--status-err); }
.plp-pct-text.pct-muted { color: var(--text-muted); } .plp-pct-text.pct-muted { color: var(--text-muted); }
/* Detail line (services + FULL indicator, below bar) */ /* FULL indicator — beside pct text in the same row */
.plp-detail {
grid-column: 2 / -1;
grid-row: 2;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.plp-full-indicator { .plp-full-indicator {
font-size: 11px; grid-column: 5;
font-weight: 700; grid-row: 1;
color: var(--status-err); font-size: 11px;
font-weight: 700;
color: var(--status-err);
background: rgba(239, 68, 68, 0.15); background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35); border: 1px solid rgba(239, 68, 68, 0.35);
padding: 1px 7px; padding: 1px 7px;
@@ -1492,28 +1586,49 @@ BUTTON VIEW DETAILS
animation: alarmPulse 1.5s ease-in-out infinite; animation: alarmPulse 1.5s ease-in-out infinite;
} }
.plp-services { /* PLP trend sparkline container — inline chart below each utilization bar.
font-size: 13px; JS renderer writes SVG into this container on each 2-sec poll.
font-weight: 600; Placed in grid row 2 (row 1 = bar + pct + FULL indicator) spanning the bar columns.
color: var(--status-neutral); Services displayed as left sidebar panel inside the SVG when present. */
max-width: 400px; .plp-trend-spark {
overflow: hidden; grid-column: 2 / -1;
text-overflow: ellipsis; grid-row: 2;
white-space: nowrap; margin-top: 4px;
overflow: visible;
} }
.plp-services:empty { /* 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; display: none;
} }
.plp-services::before { /* Sparkline SVG element — fills container width.
content: '['; ViewBox 700×120 with preserveAspectRatio="none" — stretches to match bar width.
color: var(--border-subtle); 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 { /* Max/min envelope band — semi-transparent green fill (set inline by JS). */
content: ']'; .plp-trend-svg polygon {
color: var(--border-subtle); 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) */ /* Subframe header bar (total payload at top) */
@@ -1589,22 +1704,16 @@ BUTTON VIEW DETAILS
/* Responsive adjustments for utilization bars */ /* Responsive adjustments for utilization bars */
@media (max-width: 767px) { @media (max-width: 767px) {
.plp-row { .plp-row {
grid-template-columns: 36px 1fr auto; grid-template-columns: 48px 1fr auto auto;
} }
.plp-row .plp-bitrate-text { .plp-row .plp-bitrate-text {
font-size: 11px; font-size: 11px;
} }
.plp-pct-text { .plp-pct-text {
display: none; display: none;
} }
.plp-detail { .subframe-header {
flex-wrap: wrap;
}
.plp-services {
max-width: none;
}
.subframe-header {
grid-template-columns: 48px 1fr; grid-template-columns: 48px 1fr;
} }
.subframe-header .plp-bitrate-text { .subframe-header .plp-bitrate-text {
@@ -1615,12 +1724,12 @@ BUTTON VIEW DETAILS
.global-header { .global-header {
grid-template-columns: 48px 1fr; grid-template-columns: 48px 1fr;
} }
.global-header .plp-bitrate-text { .global-header .plp-bitrate-text {
grid-column: 1 / -1; grid-column: 1 / -1;
text-align: center; text-align: center;
margin-top: 4px; margin-top: 4px;
} }
} }
/* ========================================================================== /* ==========================================================================
SESSION TIMEOUT WARNING SESSION TIMEOUT WARNING
@@ -1694,3 +1803,448 @@ SESSION TIMEOUT WARNING
.session-timeout-actions .btn-modern { .session-timeout-actions .btn-modern {
min-width: 120px; 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
<progress> 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 <html> 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 <h3> 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 <h3> 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; }
}
+69 -22
View File
@@ -14,8 +14,32 @@
// Power decimal lookup table // 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"]; 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 // Input source labels — C1 (2026-08-05): smod's OWN status map. Index 5 = SMPTE per
var INPUT_SOURCES = ["ASI1","ASI2","EAS","Tuner","TSoIP","None"]; // 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 * Calculate frequency from channel number
@@ -42,6 +66,7 @@
* @returns {number|string} Temperature in Fahrenheit * @returns {number|string} Temperature in Fahrenheit
*/ */
function convertTemperature(rawValue, unit) { function convertTemperature(rawValue, unit) {
if (!isFinite(Number(rawValue))) return "--"; // R-2: absent data -> "--", never NaN
if (unit === 0) { if (unit === 0) {
return rawValue; // Already Fahrenheit return rawValue; // Already Fahrenheit
} else { } else {
@@ -74,21 +99,32 @@
* @returns {string} Status class * @returns {string} Status class
*/ */
function getSnrStatus(snr) { function getSnrStatus(snr) {
if (snr === "N/A") return 'off'; // "N/A" and "N/A-FSSI" both mean no meaningful reading -> grey, never red.
var val = parseFloat(snr); // already carries the +10 dB display offset // (FSSI parity round; the isNaN belt matches the 1.0 core's fall-through.)
if (val >= 37) return 'ok'; if (typeof snr === "string" && snr.indexOf("N/A") === 0) return 'off';
if (val >= 30) return 'warn'; 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'; 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) * Get IMD status (UIMD/LIMD)
* @param {string} imd - IMD value or "N/A" * @param {string} imd - IMD value or "N/A"
* @returns {string} Status class * @returns {string} Status class
*/ */
function getImdStatus(imd) { function getImdStatus(imd) {
if (imd === "N/A") return 'off'; if (typeof imd === "string" && imd.indexOf("N/A") === 0) return 'off';
return parseFloat(imd) >= 47 ? 'ok' : 'err'; var v = parseFloat(imd);
if (isNaN(v)) return 'off';
return v >= 47 ? 'ok' : 'err';
} }
/** /**
@@ -126,7 +162,10 @@
(data.ctrl.toUpperCase() === "REMOTE") ? 'neutral' : 'warn'); (data.ctrl.toUpperCase() === "REMOTE") ? 'neutral' : 'warn');
setCard('sum_rf', 'dot_rf', (data.rfStat === 1) ? "On" : "Off", setCard('sum_rf', 'dot_rf', (data.rfStat === 1) ? "On" : "Off",
(data.rfStat === 1) ? 'ok' : '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 chan = Number(form[7]);
var freqVal = calcFrequency(chan); var freqVal = calcFrequency(chan);
// SNR: half-dB from the app plus the fixed +10 dB display offset. // SNR: half-dB value from the app, displayed as the true measured SNR/MER.
var snr = (ab_modenum === 1) ? "N/A" : (Number(form[11]) / 2 + 10).toFixed(1); 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. // LIMD / UIMD are half-dB values from the app. Apply the fixed +11 dB
// Rounding the sum and then appending the half misreports odd half-dB // display offset used by Status and the LCD.
// values by 1 dB, so format the sum directly. var limd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[12], function(n){ return (n / 2 + 11).toFixed(1); });
var limd = (ab_modenum === 1) ? "N/A" : (Number(form[12]) / 2 + 11).toFixed(1); var uimd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[13], function(n){ return (n / 2 + 11).toFixed(1); });
var uimd = (ab_modenum === 1) ? "N/A" : (Number(form[13]) / 2 + 11).toFixed(1);
// Input source // The status interface already carries the shared A/B FSSI values.
var inputIdx = Number(form[1]); // Spectrum consumes the same values instead of issuing another request.
if (inputIdx < 0 || inputIdx > 5) inputIdx = 5; updateSpectrumFssi(Number(form[16]), Number(form[17]), ab_modenum);
var inputSrc = INPUT_SOURCES[inputIdx]; 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 // Input status
var inputStatus = (Number(form[2]) === 0) ? "OK" : "Err"; var inputStatus = (Number(form[2]) === 0) ? "OK" : "Err";
@@ -222,7 +267,8 @@
var p_sign = (Number(rf_form[1]) === 0) ? "+" : "-"; var p_sign = (Number(rf_form[1]) === 0) ? "+" : "-";
var p_int = Number(rf_form[2]); var p_int = Number(rf_form[2]);
var p_dec = POWER_DECIMALS[Number(rf_form[3])] || "00"; 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 // Update all cards using the shared function
window.updateDashboardCards({ window.updateDashboardCards({
@@ -254,7 +300,8 @@
getSnrStatus: getSnrStatus, getSnrStatus: getSnrStatus,
getImdStatus: getImdStatus, getImdStatus: getImdStatus,
POWER_DECIMALS: POWER_DECIMALS, POWER_DECIMALS: POWER_DECIMALS,
INPUT_SOURCES: INPUT_SOURCES INPUT_SOURCES: INPUT_SOURCES,
getInputSource: getInputSource
}; };
})(); })();
+49
View File
@@ -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 <head> 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 <html> 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 <head>, 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 */ }
};
})();
+23 -13
View File
@@ -4,7 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle"></title>
<link href="css/styles.css" rel="stylesheet" media="screen"> <script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
@@ -299,12 +300,13 @@
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li> <li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li class="active"><a href="log_output.html" title="Log">Log</a></li> <li class="active"><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -324,7 +326,14 @@
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -345,6 +354,7 @@
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -371,16 +381,6 @@
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -391,6 +391,16 @@
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
+16 -9
View File
@@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle">Digital Exciter</title>
<script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen"> <link href="css/styles.css" rel="stylesheet" media="screen">
<style> <style>
#content .box table td:first-child { #content .box table td:first-child {
@@ -40,13 +41,13 @@ function loginsubmit() {
} }
function refresh_show(){ function refresh_show(){
var queryData = "web_title=&" /* atsc_status_cfg.cgi requires a session, but the login page has none yet. */
$.get("atsc_status_cfg.cgi?"+queryData, function(data, status) { $.get("login.cgi?action=get_title", function(data) {
var table = data.split("|"); var titleText = String(data || "").replace(/^#/, "").trim() || "Digital Exciter";
var title = table[0].split("#"); document.title = titleText;
document.getElementById('webtitle').textContent = title[1]; document.getElementById('webtitle').textContent = titleText;
document.getElementById('webtitle_header').textContent = title[1]; document.getElementById('webtitle_header').textContent = titleText;
}); }, "text");
} }
</script> </script>
@@ -73,7 +74,13 @@ function refresh_show(){
</svg> </svg>
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span>Digital Exciter</span></h2>
<h3 class="has-toggle" style="margin-left:auto"><div class="utility-cluster"><button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button></div></h3>
</header> </header>
<main class="app-main"> <main class="app-main">
File diff suppressed because it is too large Load Diff
+140 -56
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle"></title>
<script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen"> <link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
@@ -41,7 +42,19 @@ function get_remote_local_ctrl() {
var table = data.split("|"); var table = data.split("|");
// Logic for remote/local control if needed // Logic for remote/local control if needed
// g_remot_local_ctrl = Number(table[0].split("#")[5]); // 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() { 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=&" 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) { $.get("atsc_status_cfg.cgi?"+queryData, function(data, status) {
var temp_unit=1; var temp_unit=1;
@@ -63,10 +81,23 @@ function SetFormDefaults() {
var ab_modenum = Number(form[25]); var ab_modenum = Number(form[25]);
temp_unit = Number(form[26]); temp_unit = Number(form[26]);
// Input Source Map // Input Source — C1 (2026-08-05): the page-private ternary is DELETED; this and the
var i = Number(form[1]); // dashboard cards read the ONE map via DashboardHelpers.getInputSource (the two lists
var s_str = (i==0)?"ASI1":(i==1)?"ASI2":(i==2)?"EAS":(i==3)?"Tuner":(i==4)?"TSoIP":(i==5)?"SMPTE":"None"; // disagreed at index 5 — SMPTE vs None — which is your "None for a selected input").
cf.asi_input.value = s_str; 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 // Input Status
cf.asi_status.value = (Number(form[2])==0) ? "OK" : "Err"; cf.asi_status.value = (Number(form[2])==0) ? "OK" : "Err";
@@ -76,12 +107,15 @@ function SetFormDefaults() {
cf.ts_rate.value = form[4]; // TS Rate cf.ts_rate.value = form[4]; // TS Rate
// Temperature // Temperature — R-2: absent data -> "--", never NaN cascades
if(temp_unit==0){ 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.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 { } 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]; cf.temp1_c.value = form[5];
} }
@@ -102,27 +136,27 @@ function SetFormDefaults() {
// Offset // Offset
i = Number(form[8]); 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]; cf.hz1.value = s_str + form[9];
// FWD Power & SNR // FWD Power & SNR
var rawFwd = Number(form[10]); 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 // SNR. Half-dB from the app, shown as-is (PR12 removed the old fixed
// +10 dB display offset; this is the true measured MER). // +10 dB display offset; this is the true measured MER).
var tmpdata = Number(form[11]); var tmpdata = Number(form[11]);
var snrStr = (tmpdata/2).toFixed(1); 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. // 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 // (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.) // half-dB value that rounds x.5 up and reports 1 dB too much.)
tmpdata = Number(form[12]); 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]); 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 // Control & Amp
cf.f1ctrl.value = (Number(form[14])==0) ? "Remote" : "Local"; 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"; cf.b_fssi.value = (ab_modenum==1) ? (Number(form[17]) + " %") : "N/A";
// AGC State // 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]); agc_err_code = Number(form[27]);
var agc_adj = Number(form[28]); var agc_adj = Number(form[28]);
if(agc_err_code==1) s_str="Down Limit!"; if(agc_err_code==1) s_str="Down Limit!";
@@ -154,6 +195,7 @@ function SetFormDefaults() {
s_str="OK"; s_str="OK";
} }
} }
}
cf.agc_state.value = s_str; cf.agc_state.value = s_str;
// --- 2. PARSE SYS_RF FOR DASHBOARD --- // --- 2. PARSE SYS_RF FOR DASHBOARD ---
@@ -191,37 +233,40 @@ function SetFormDefaults() {
// Form 1: Tuner // Form 1: Tuner
var form1 = table[1].split("#"); var form1 = table[1].split("#");
var cf1 = document.getElementById("tuner"); var cf1 = document.getElementById("tuner");
cf1.rx_freq.value = Number(form1[1]); cf1.rx_freq.value = fmtRaw(form1[1]);
cf1.rssi.value = Number(form1[2]); cf1.rssi.value = fmtRaw(form1[2]);
cf1.rsnr.value = Number(form1[3]); cf1.rsnr.value = fmtRaw(form1[3]);
cf1.lock.selectedIndex = Number(form1[4]); cf1.lock.selectedIndex = Number(form1[4]);
// Form 2: PAC Status // Form 2: PAC Status
var form2 = table[2].split("#"); var form2 = table[2].split("#");
var cf2 = document.getElementById("pac_status"); var cf2 = document.getElementById("pac_status");
cf2.v9v.value = (Number(form2[1])*0.1).toFixed(2); cf2.v9v.value = fmtScaled(form2[1], 0.1, 2);
cf2.v12v.value = (Number(form2[2])*0.1).toFixed(2); cf2.v12v.value = fmtScaled(form2[2], 0.1, 2);
cf2.v50v.value = (Number(form2[3])*0.1).toFixed(2); cf2.v50v.value = fmtScaled(form2[3], 0.1, 2);
cf2.pac_fwd.value = (Number(form2[4])*0.1).toFixed(2); cf2.pac_fwd.value = fmtScaled(form2[4], 0.1, 2);
cf2.ref.value = (Number(form2[5])*0.1).toFixed(2); cf2.ref.value = fmtScaled(form2[5], 0.1, 2);
cf2.vswr.value = (Number(form2[6])*0.1).toFixed(2); cf2.vswr.value = fmtScaled(form2[6], 0.1, 2);
// PAC Temp // PAC Temp — R-2 guarded
if(temp_unit==0){ var pac_t = Number(form2[7]);
cf2.temp.value = (Number(form2[7])*0.1).toFixed(2); 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); cf2.temp_c.value = (cf2.temp.value < 32) ? 0 : ((cf2.temp.value - 32)/1.8).toFixed(2);
} else { } else {
cf2.temp.value = (Number(form2[7])*0.1*1.8+32).toFixed(2); cf2.temp.value = (pac_t*0.1*1.8+32).toFixed(2);
cf2.temp_c.value = (Number(form2[7])*0.1).toFixed(2); cf2.temp_c.value = (pac_t*0.1).toFixed(2);
} }
cf2.pain.value = Number(form2[8]); // This is PA_IN cf2.pain.value = Number(form2[8]); // This is PA_IN
cf2.c50v1.value = (Number(form2[9])*0.1).toFixed(2); cf2.c50v1.value = fmtScaled(form2[9], 0.1, 2);
cf2.c50v2.value = (Number(form2[10])*0.1).toFixed(2); cf2.c50v2.value = fmtScaled(form2[10], 0.1, 2);
cf2.c50v3.value = (Number(form2[11])*0.1).toFixed(2); cf2.c50v3.value = fmtScaled(form2[11], 0.1, 2);
cf2.c50v4.value = (Number(form2[12])*0.1).toFixed(2); cf2.c50v4.value = fmtScaled(form2[12], 0.1, 2);
cf2.gv.value = (Number(form2[13])*0.1).toFixed(2); cf2.gv.value = fmtScaled(form2[13], 0.1, 2);
cf2.lvl.selectedIndex = Number(form2[14]); cf2.lvl.selectedIndex = Number(form2[14]);
// Form 3: GPS // Form 3: GPS
@@ -252,9 +297,12 @@ function SetFormDefaults() {
if (table[7]) { if (table[7]) {
var title = table[7].split("#"); var title = table[7].split("#");
var titleStr = (title[1] != null) ? title[1] : ""; var titleStr = (title[1] != null) ? title[1] : "";
/* 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').textContent = titleStr;
document.getElementById('webtitle_header').textContent = titleStr; document.getElementById('webtitle_header').textContent = titleStr;
} }
}
// Update Status Colors and UI helpers // Update Status Colors and UI helpers
updateStatusColors(); updateStatusColors();
@@ -330,14 +378,15 @@ function highlightProblemTableRows() {
} }
// SNR, LIMD, UIMD: same error thresholds as summary cards / updateStatusColors // SNR, LIMD, UIMD: same error thresholds as summary cards / updateStatusColors
var id = inp.id || ''; 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) var snrNum = parseFloat(v); // true measured dB (PR12: no display offset)
if (snrNum < 15) { tr.classList.add('row-has-error'); break; } 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 (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; } if (parseFloat(v) < 47) { tr.classList.add('row-has-error'); break; }
} }
// License fields: INACTIVE is an error // 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) // Main Status details: LIMD, UIMD, SNR – same rules as summary cards (green/red/warn by range)
var tsnrEl = document.getElementById("tsnr"); 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) var snrVal = parseFloat(tsnrEl.value); // true measured dB (PR12: no display offset)
if(snrVal >= 27) tsnrEl.style.color = color_ok; if(snrVal >= 27) tsnrEl.style.color = color_ok;
else if(snrVal >= 20) tsnrEl.style.color = color_warn; else if(snrVal >= 20) tsnrEl.style.color = color_warn;
@@ -389,12 +438,12 @@ function updateStatusColors() {
} else if(tsnrEl) tsnrEl.style.color = ""; } else if(tsnrEl) tsnrEl.style.color = "";
var limdEl = document.getElementById("l_imd"); 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; limdEl.style.color = parseFloat(limdEl.value) >= 47 ? color_ok : color_err;
} else if(limdEl) limdEl.style.color = ""; } else if(limdEl) limdEl.style.color = "";
var uimdEl = document.getElementById("u_imd"); 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; uimdEl.style.color = parseFloat(uimdEl.value) >= 47 ? color_ok : color_err;
} else if(uimdEl) uimdEl.style.color = ""; } 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() { function initCollapsibleTables() {
var headers = document.querySelectorAll('.app-main h2'); var headers = document.querySelectorAll('.app-main h2');
for (var i = 0; i < headers.length; i++) { for (var i = 0; i < headers.length; i++) {
@@ -621,20 +691,26 @@ function initCollapsibleTables() {
} }
if (box) { 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'); var boxHeader = document.createElement('div');
boxHeader.className = "box-header collapsed"; boxHeader.className = startOpen ? "box-header" : "box-header collapsed";
var titleSpan = document.createElement('span'); var titleSpan = document.createElement('span');
titleSpan.innerText = headerText + " Details"; titleSpan.innerText = headerText + " Details";
var toggleSpan = document.createElement('span'); var toggleSpan = document.createElement('span');
toggleSpan.className = "toggle-text"; toggleSpan.className = "toggle-text";
toggleSpan.innerText = "View Details"; toggleSpan.innerText = startOpen ? "Hide Details" : "View Details";
boxHeader.appendChild(titleSpan); boxHeader.appendChild(titleSpan);
boxHeader.appendChild(toggleSpan); boxHeader.appendChild(toggleSpan);
var contentDiv = document.createElement('div'); var contentDiv = document.createElement('div');
contentDiv.className = "box-content"; contentDiv.className = "box-content";
contentDiv.style.display = "none"; contentDiv.style.display = startOpen ? "block" : "none";
while (box.firstChild) { while (box.firstChild) {
contentDiv.appendChild(box.firstChild); contentDiv.appendChild(box.firstChild);
@@ -683,17 +759,17 @@ document.addEventListener('DOMContentLoaded', function() {
</script> </script>
</head> </head>
<body onload="refresh_show()">
<body onload="refresh_show()"> <body onload="refresh_show()">
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li class="active"><a href="status_info.html" title="Status">Status</a></li> <li class="active"><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li> <li><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -713,7 +789,14 @@ document.addEventListener('DOMContentLoaded', function() {
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -734,6 +817,7 @@ document.addEventListener('DOMContentLoaded', function() {
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -760,16 +844,6 @@ document.addEventListener('DOMContentLoaded', function() {
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -780,6 +854,16 @@ document.addEventListener('DOMContentLoaded', function() {
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
+118 -16
View File
@@ -4,7 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle"></title>
<link href="css/styles.css" rel="stylesheet" media="screen"> <script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
@@ -57,10 +58,99 @@
var table = data.split("|"); var table = data.split("|");
g_remot_local_ctrl = Number(table[0].split("#")[5]); 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 // Updated SetFormDefaults to populate the Single Form
function SetFormDefaults() { function SetFormDefaults() {
// 1. DISABLE BUTTONS START (only in Remote mode) // 1. DISABLE BUTTONS START (only in Remote mode)
@@ -133,6 +223,9 @@ function SetFormDefaults() {
} }
if (typeof setTsidFormSnapshotUpdate === 'function') setTsidFormSnapshotUpdate(); if (typeof setTsidFormSnapshotUpdate === 'function') setTsidFormSnapshotUpdate();
if (typeof psipFormSnapshotUpdate === 'function') psipFormSnapshotUpdate(); 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').textContent = title[1];
document.getElementById('webtitle_header').textContent = title[1]; document.getElementById('webtitle_header').textContent = title[1];
@@ -474,12 +567,13 @@ function cmd_confirm() {
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li> <li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li class="active"><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li class="active"><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li> <li><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -499,7 +593,14 @@ function cmd_confirm() {
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -520,6 +621,7 @@ function cmd_confirm() {
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -546,16 +648,6 @@ function cmd_confirm() {
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -566,6 +658,16 @@ function cmd_confirm() {
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
@@ -659,7 +761,7 @@ function cmd_confirm() {
</select> </select>
</td> </td>
<td> <td>
<input type="button" class="btn-modern" alt="Set" value="Set" disabled onclick="return tsid_psip_top_set();"> <input type="button" id="ts_ctrl_set" class="btn-modern" alt="Set" value="Set" disabled onclick="return tsid_psip_top_set();">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -668,7 +770,7 @@ function cmd_confirm() {
<input type="text" maxlength="5" size="8" name="tsid" id="tsid" value=""> <input type="text" maxlength="5" size="8" name="tsid" id="tsid" value="">
</td> </td>
<td> <td>
<input type="button" class="btn-modern" alt="Set" value="Set" disabled onclick="return tsid_psip_top_set();"> <input type="button" id="ts_tsid_set" class="btn-modern" alt="Set" value="Set" disabled onclick="return tsid_psip_top_set();">
</td> </td>
</tr> </tr>
</table> </table>
+86 -20
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle"></title>
<script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen"> <link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
@@ -66,7 +67,11 @@ function get_remote_local_ctrl() {
var table = data.split("|"); var table = data.split("|");
g_remot_local_ctrl = Number(table[0].split("#")[5]); 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') {
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li> <li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li class="active"><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li class="active"><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li> <li><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -491,7 +497,14 @@ if (document.readyState === 'loading') {
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -512,6 +525,7 @@ if (document.readyState === 'loading') {
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -538,16 +552,6 @@ if (document.readyState === 'loading') {
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -558,6 +562,16 @@ if (document.readyState === 'loading') {
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
@@ -696,6 +710,21 @@ if (document.readyState === 'loading') {
</td> </td>
</tr> </tr>
<!-- AGC / FSSI interaction (Session 4, 2026-08-11 — owner decision: LEAVE UNGATED).
DECISION: AGC stays freely settable on this page, including while FSSI Refresh is On.
ANALYSIS behind the decision (recorded so a future session does not "fix" this blindly):
- Under FSSI, AGC state is not physically meaningful: "FSSI active alternates the
RFinA feedback path AGC realizes on" (status_info.html physics comment, identical in
the 1.0 and 3.0). The status page already shows AGC as "N/A-FSSI" when FSSI is On.
- The family's hard interlock is FSSI<->ADPC, not FSSI<->AGC. smod HAS that ADPC gate
(sys_set.html: blocks ADPC transitions while FSSI Refresh On; 3.0 sys_set.html:858
"do not relax"). ADPC and AGC share the same RFinA feedback loop, so gating ADPC was
the intended guard; AGC is covered indirectly through it.
- NONE of the three products (smod, 1.0, 3.0) gates AGC on FSSI at the control layer —
so this is consistent family behaviour, not a smod-unique omission.
Owner ruled 2026-08-11 to keep AGC ungated (the ADPC interlock is the guard). If this is
ever revisited, the RF page would need to poll FSSI state (it does not today) — see the
S4 ledger append and S5 handoff for the full reasoning. -->
<tr> <tr>
<td>AGC</td> <td>AGC</td>
<td> <td>
@@ -882,7 +911,30 @@ if (document.readyState === 'loading') {
</table> </table>
</form> </form>
<!-- This warning is ABOUT the TX Frequency control above it.
The PR07 Reference Output section had been inserted between them, pushing it
to the bottom of the page, away from the control it describes (on the 1.0 it
sits adjacent because nothing intervenes). Moved back to sit under its control. -->
<!-- R8 (2026-08-11): STANDING informational warning -> plain amber TEXT, matching the 3.0
exactly (3.0 sys_rf.html:974). The family rule, read off the 3.0's own pages:
- plain amber text (color:var(--status-warn), no box) = a STANDING FACT about how a
control behaves (this note, and the 10 MHz note below). Always visible.
- .warning-banner (tinted box) = a CONDITIONAL mode/section advisory that appears and
disappears (e.g. the 3.0's hidden "Gateway RF-In mode" note).
- .warning-banner.is-error (red box) = a genuine fault.
R3/R4 wrongly put this standing note in the tinted box; the 3.0 never did. Corrected. -->
<div style="padding: 10px; color: var(--status-warn); font-size: 12px;">
Warning: Exciter RF Output Power will be reset to -25 dBm when the channel (TX Frequency) is changed.
</div>
</div><!-- /.box — P-7 (2026-08-06): THE STRUCTURAL DEFECT. The RF Control card
was never closed here, so the PR07 "Reference Output" section (h2, table and
note) rendered INSIDE the RF Control card — an h2 with a full-width rule
sitting in another card's padding, which is the "doesn't fit / out of the
picture" the owner reported. Closing it makes Reference Output a sibling
section: h2 then its own .box, exactly like every other section on sys_set. -->
<h2>Reference Output</h2> <h2>Reference Output</h2>
<div class="box">
<table> <table>
<tr> <tr>
<td>10 MHz Output</td> <td>10 MHz Output</td>
@@ -897,21 +949,35 @@ if (document.readyState === 'loading') {
</td> </td>
</tr> </tr>
</table> </table>
<div style="padding: 4px 10px; color: #64748b; font-size: 12px;"> <!-- R8: standing informational note -> plain amber text, same rule as the -25 dBm note above. -->
Enables the AB5 10 MHz reference clock output. Default Off; resets to Off on every power-up. <!-- R11 (2026-08-17, owner-ruled). SUPERSEDES the R9/D-27.3 and D-28.1 rulings above, which
decided WHERE the word "Warning:" sat before either of us had read the sentence for sense.
History worth keeping: this note was chased for several rounds as a "missing Warning:"
bug. It was never missing — the word was present in source AND in the unit's own served
bytes (owner F12 Network->Response capture, 2026-08-17). Both of us mis-read it, because
both of us expected "Warning:" at the START of the sentence, as it is on the -25 dBm note
above; here it sat mid-sentence and the eye slid past it.
Reading it for SENSE surfaced the real defect: the sentence opened like a command
("Enables...") and then hung "Warning:" in front of a NON-hazard.
THE GOVERNING RULE, which applies to any future note: a standing note earns "Warning:"
only when it flags a REAL CONSEQUENCE; reserve the word so the label keeps discriminating.
This matters here specifically because the tree's only two prose notes (this one and the
-25 dBm note above) share ONE identical amber style — so COLOUR cannot distinguish a
hazard from a heads-up, and the LABEL is the only thing that can.
Hence: the -25 dBm note keeps "Warning:" (real consequence — silent power loss on a
channel change); this one becomes "Note:" (behavioural heads-up only). Styling unchanged. -->
<div style="padding: 6px 10px 2px; color: var(--status-warn); font-size: 12px;">
Note: The 10 MHz reference clock output is Off by default, and resets to Off on every power-up.
</div> </div>
</div><!-- /.box Reference Output -->
<div style="padding: 10px; color: #b91c1c; font-weight: 500;">
Warning: Exciter RF Output Power will be reset to -25dBm when the channel(TX Frequency) is changed.
</div>
</div>
</main> </main>
</div> </div>
<div id="accountModal" class="modal"> <div id="accountModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3>ACCOUNT SETTINGS</h3> <h3>Account Settings</h3>
<span class="close" onclick="closeAccountModal()">&times;</span> <span class="close" onclick="closeAccountModal()">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
+306 -63
View File
@@ -3,8 +3,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle">Digital Exciter</title>
<link href="css/styles.css" rel="stylesheet" media="screen"> <script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
@@ -13,13 +14,26 @@
.spec-toolbar { display:flex; flex-wrap:wrap; gap:18px; align-items:center; margin-bottom:10px; } .spec-toolbar { display:flex; flex-wrap:wrap; gap:18px; align-items:center; margin-bottom:10px; }
.spec-toolbar label { font-size:13px; color:inherit; } .spec-toolbar label { font-size:13px; color:inherit; }
.spec-readouts { display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; } .spec-readouts { display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; }
.spec-readout { min-width:130px; padding:8px 12px; border:1px solid rgba(148,163,184,0.35); border-radius:6px; } .spec-readout { min-width:110px; padding:6px 8px; border:1px solid rgba(148,163,184,0.35); border-radius:6px; }
.spec-readout .k { display:block; font-size:11px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; } .spec-readout-pair { display:flex; gap:10px; flex:0 0 auto; }
.spec-readout .v { display:block; font-size:18px; font-weight:600; } .spec-readout .k { display:block; font-size:10px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; }
#spec_canvas { width:100%; height:420px; display:block; background:#0b1220; border-radius:6px; } .spec-readout .v { display:block; font-size:16px; font-weight:600; }
#eye_canvas { width:100%; height:380px; display:block; background:#0b1220; border-radius:6px; } #spectrum_panel .spec-readouts { gap:8px; overflow-x:auto; padding-bottom:2px; }
#spectrum_panel .spec-readout-pair { gap:8px; }
#spec_canvas { width:100%; height:420px; display:block; background:var(--plot-bg); border-radius:6px; }
#eye_canvas { width:100%; height:380px; display:block; background:var(--plot-bg); border-radius:6px; }
#eye_state { font-size:13px; opacity:.7; margin-left:auto; } #eye_state { font-size:13px; opacity:.7; margin-left:auto; }
#spec_state { font-size:13px; opacity:.7; margin-left:auto; } #spec_state { font-size:13px; opacity:.7; margin-left:auto; }
.spec-tabs { display:flex; gap:0; border-bottom:1px solid rgba(148,163,184,.35); margin-bottom:16px; }
.spec-tab { appearance:none; border:0; border-bottom:3px solid transparent; background:transparent; color:inherit; cursor:pointer; font:inherit; font-weight:600; padding:10px 18px; }
.spec-tab:hover { background:rgba(148,163,184,.10); }
.spec-tab.active { border-bottom-color:var(--status-neutral); color:var(--status-neutral); }
.spectrum-panel[hidden] { display:none; }
.fssi-refresh-notice { display:none; padding:44px 18px; text-align:center; color:#fbbf24; font-weight:600; }
.spectrum-panel.fssi-refresh-active canvas,
.spectrum-panel.fssi-refresh-active .spec-readouts,
.spectrum-panel.fssi-refresh-active .spec-toolbar { display:none; }
.spectrum-panel.fssi-refresh-active .fssi-refresh-notice { display:block; }
</style> </style>
<script> <script>
var lastTime = new Date().getTime(); var lastTime = new Date().getTime();
@@ -46,7 +60,11 @@
$.get("atsc_status_cfg.cgi?sys_cfg=&", function(data, status){ $.get("atsc_status_cfg.cgi?sys_cfg=&", function(data, status){
if (typeof data === 'object' && data !== null) return; if (typeof data === 'object' && data !== null) return;
var table = String(data).split("|"); var table = String(data).split("|");
document.title = table[0].split("#")[11]; var sysCfg = table[0].split("#");
var title = sysCfg[6] || "Digital Exciter";
document.title = title;
document.getElementById('webtitle').textContent = title;
document.getElementById('webtitle_header').textContent = title;
}); });
} }
@@ -70,22 +88,71 @@
var Y_TOP = 30, Y_BOT = -80; // dB rel in-band flat density var Y_TOP = 30, Y_BOT = -80; // dB rel in-band flat density
var SH_LO = 3.5, SH_HI = 4.8; // shoulder band the app takes the median over var SH_LO = 3.5, SH_HI = 4.8; // shoulder band the app takes the median over
var activeView = 'spectrum';
var fssiRefreshActive = false;
function specInit(){ function specInit(){
window.addEventListener('resize', function(){ if (specLast) drawSpectrum(specLast); else drawGrid(); }); window.addEventListener('resize', function(){
drawGrid(); if (activeView === 'spectrum') {
specRestart(); if (specLast) drawSpectrum(specLast); else drawGrid();
eyeRestart(); } else if (eyeLastData) {
drawEye(eyeLastData);
}
});
selectSpectrumView('spectrum');
} }
/* PR12: the Refresh/Pause/Peak-hold toolbar and the status lines were function selectSpectrumView(view) {
* removed on request, so these helpers tolerate the elements being absent activeView = (view === 'eye') ? 'eye' : 'spectrum';
* (fixed 1 s refresh, never paused, no peak-hold). */ var spectrumActive = activeView === 'spectrum';
document.getElementById('spectrum_panel').hidden = !spectrumActive;
document.getElementById('eye_panel').hidden = spectrumActive;
document.getElementById('spectrum_tab').classList.toggle('active', spectrumActive);
document.getElementById('eye_tab').classList.toggle('active', !spectrumActive);
document.getElementById('spectrum_tab').setAttribute('aria-selected', spectrumActive);
document.getElementById('eye_tab').setAttribute('aria-selected', !spectrumActive);
if (fssiRefreshActive) {
setState('FSSI Refresh ON');
setEyeState('FSSI Refresh ON');
return;
}
if (spectrumActive) {
if (EYE_TIMER) clearInterval(EYE_TIMER);
EYE_TIMER = null;
if (specLast) drawSpectrum(specLast); else drawGrid();
specRestart();
} else {
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
SPEC_TIMER = null;
if (eyeLastData) drawEye(eyeLastData);
eyeRestart();
}
}
/* Spectrum controls are shared by both tabs. */
function elChecked(id){ var e = document.getElementById(id); return !!(e && e.checked); } function elChecked(id){ var e = document.getElementById(id); return !!(e && e.checked); }
function specRateMs(){ var e = document.getElementById('spec_rate'); return e ? Number(e.value) : 1000; } function specRateMs(){ var e = document.getElementById('spec_rate'); return e ? Number(e.value) : 1000; }
function spectrumControlsChanged(){
if (activeView === 'spectrum') {
specRestart();
} else {
eyeRestart();
if (elChecked('spec_pause')) setEyeState('paused');
}
}
function peakHoldChanged(){
specPeak = null;
if (activeView === 'spectrum' && specLast) drawSpectrum(specLast);
}
function specRestart(){ function specRestart(){
if (SPEC_TIMER) clearInterval(SPEC_TIMER); if (SPEC_TIMER) clearInterval(SPEC_TIMER);
if (activeView !== 'spectrum') return;
if (fssiRefreshActive) { setState('FSSI Refresh ON'); return; }
if (elChecked('spec_pause')) { setState('paused'); return; } if (elChecked('spec_pause')) { setState('paused'); return; }
specFetch(); specFetch();
SPEC_TIMER = setInterval(specFetch, specRateMs()); SPEC_TIMER = setInterval(specFetch, specRateMs());
@@ -94,6 +161,26 @@
function setState(s){ var e = document.getElementById('spec_state'); if (e) e.innerText = s; } function setState(s){ var e = document.getElementById('spec_state'); if (e) e.innerText = s; }
function setEyeState(s){ var e = document.getElementById('eye_state'); if (e) e.innerText = s; } function setEyeState(s){ var e = document.getElementById('eye_state'); if (e) e.innerText = s; }
window.setSpectrumRefreshActive = function(active){
active = !!active;
if (fssiRefreshActive === active) return;
fssiRefreshActive = active;
document.getElementById('spectrum_panel').classList.toggle('fssi-refresh-active', active);
document.getElementById('eye_panel').classList.toggle('fssi-refresh-active', active);
if (active) {
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
if (EYE_TIMER) clearInterval(EYE_TIMER);
SPEC_TIMER = null;
EYE_TIMER = null;
setState('FSSI Refresh ON');
setEyeState('FSSI Refresh ON');
} else if (activeView === 'spectrum') {
specRestart();
} else {
eyeRestart();
}
};
function specFetch(){ function specFetch(){
$.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false}) $.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false})
.done(function(d){ .done(function(d){
@@ -102,18 +189,59 @@
setState(d && d.error === 'nodata' ? 'no window captured yet' : 'error'); setState(d && d.error === 'nodata' ? 'no window captured yet' : 'error');
return; return;
} }
if (fssiRefreshActive) return;
specFails = 0; specFails = 0;
specLast = d; specLast = d;
if (activeView === 'spectrum') {
updateReadouts(d); updateReadouts(d);
drawSpectrum(d); drawSpectrum(d);
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString()); setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
}
}) })
.fail(function(){ specFails++; setState('fetch failed (' + specFails + ')'); }); .fail(function(){ specFails++; setState('fetch failed (' + specFails + ')'); });
} }
function updateReadouts(d){ function updateReadouts(d){
/* PR12: the readout cards under the spectrum were removed; d.shoulder /* R7 (2026-08-11) + R10 (2026-08-12): apply the documented 11 dB pilot offset
* and d.pilot are still drawn on the plot itself in drawSpectrum(). */ so these readouts match the dashboard's LIMD/UIMD and the 1.0 convention.
SIGN CONVENTION (R10, two-level hardware-verified): d.limd/d.uimd are POSITIVE
attenuation magnitudes -> offset is +11. d.shoulder is a SIGNED level below
in-band flat density (negative) -> offset is -11 (more suppression = more
negative). Verified: raw -22.7 -> -33.7 vs IMD 32.3/32.5; raw ~-63.4 -> -74.4
vs IMD 73.6/73.9. */
var IMD_PILOT_OFFSET = 11; // dB; documented pilot offset (1.0 status_info_new.html)
setReadout('rd_shoulder', d.shoulder - IMD_PILOT_OFFSET, 1);
setReadout('rd_limd', d.limd + IMD_PILOT_OFFSET, 1);
setReadout('rd_uimd', d.uimd + IMD_PILOT_OFFSET, 1);
setReadout('rd_pilot', d.pilot, 4);
setReadout('rd_peak', d.peak, 1);
setReadout('rd_rbw', d.rbw_khz, 2);
setReadout('rd_ms', d.ms, 2);
var packets = document.getElementById('rd_pkts');
if (packets) {
packets.innerText = (d.pkts === undefined || d.npkt === undefined) ? '--' : d.pkts + '/' + d.npkt;
packets.style.color = (d.pkts === d.npkt) ? '' : 'var(--status-err)';
}
var field = document.getElementById('rd_field');
var contiguous = readNumber(d.ms);
if (field) {
field.innerText = (contiguous === null) ? '--' : (contiguous > 24.2 ? 'yes' : 'no');
field.style.color = (contiguous === null) ? '' : (contiguous > 24.2 ? 'var(--accent-green)' : 'var(--status-err)');
}
}
function readNumber(value){
var number = Number(value);
return isFinite(number) ? number : null;
}
function setReadout(id, value, decimals){
var element = document.getElementById(id);
var number = readNumber(value);
if (element) element.innerText = (number === null) ? '--' : number.toFixed(decimals);
return number;
} }
@@ -128,6 +256,8 @@
function eyeRestart(){ function eyeRestart(){
if (EYE_TIMER) clearInterval(EYE_TIMER); if (EYE_TIMER) clearInterval(EYE_TIMER);
if (activeView !== 'eye') return;
if (fssiRefreshActive) { setEyeState('FSSI Refresh ON'); return; }
if (elChecked('spec_pause')) return; if (elChecked('spec_pause')) return;
eyeFetch(); eyeFetch();
EYE_TIMER = setInterval(eyeFetch, specRateMs()); EYE_TIMER = setInterval(eyeFetch, specRateMs());
@@ -142,24 +272,47 @@
return; return;
} }
if (!d.sync){ setEyeState('no symbol lock'); return; } if (!d.sync){ setEyeState('no symbol lock'); return; }
if (fssiRefreshActive) return;
eyeFails = 0; eyeFails = 0;
eyeLastData = d; eyeLastData = d;
if (activeView === 'eye') {
eyeReadouts(d); eyeReadouts(d);
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); } if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols'); setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
}
}) })
.fail(function(){ eyeFails++; setEyeState('fetch failed (' + eyeFails + ')'); }); .fail(function(){ eyeFails++; setEyeState('fetch failed (' + eyeFails + ')'); });
} }
function eyeReadouts(d){ function eyeReadouts(d){
/* PR12: only SNR/MER and SNR raw remain on request. */ var eyeHeights = Array.isArray(d.eye_h) ? d.eye_h.map(readNumber).filter(function(value){ return value !== null; }) : [];
document.getElementById('rd_snr').innerText = d.snr_db.toFixed(2); var sigma = Array.isArray(d.sigma) ? d.sigma.map(readNumber).filter(function(value){ return value !== null; }) : [];
document.getElementById('rd_snrraw').innerText = (d.snr_raw_db === undefined) ? '--' : d.snr_raw_db.toFixed(2); var minh = eyeHeights.length ? Math.min.apply(null, eyeHeights) : null;
var sAvg = sigma.length ? sigma.reduce(function(a, b){ return a + b; }, 0) / sigma.length : null;
var snr = setReadout('rd_snr', d.snr_db, 2);
setReadout('rd_eyeh', minh, 2);
setReadout('rd_eyew', d.eye_w_ui, 3);
setReadout('rd_tau', d.tau, 4);
setReadout('rd_segsync', d.segsync_ratio, 1);
setReadout('rd_coff', d.carrier_off_hz, 3);
setReadout('rd_snrraw', d.snr_raw_db, 2);
var jitter = setReadout('rd_gjit', d.gain_jitter_pct, 2);
var sn = document.getElementById('rd_snr'); var sn = document.getElementById('rd_snr');
sn.style.color = (d.snr_db >= 27) ? '#22c55e' : (d.snr_db >= 20 ? '#f59e0b' : '#ef4444'); if (sn) sn.style.color = (snr === null) ? '' : (snr >= 27 ? 'var(--accent-green)' : (snr >= 20 ? 'var(--status-warn)' : 'var(--status-err)'));
var height = document.getElementById('rd_eyeh');
if (height) height.style.color = (minh === null || sAvg === null) ? '' :
((minh > 0.6 && minh > 2 - 6 * sAvg - 0.35) ? 'var(--accent-green)' : (minh > 0.3 ? 'var(--status-warn)' : 'var(--status-err)'));
var jitterElement = document.getElementById('rd_gjit');
if (jitterElement) jitterElement.style.color = (jitter === null) ? '' :
(jitter < 1.0 ? 'var(--accent-green)' : (jitter < 4 ? 'var(--status-warn)' : 'var(--status-err)'));
} }
function drawEye(d){ function drawEye(d){
var P = PLOT();
var c = document.getElementById('eye_canvas'); var c = document.getElementById('eye_canvas');
var dpr = window.devicePixelRatio || 1; var dpr = window.devicePixelRatio || 1;
var W = c.clientWidth, H = c.clientHeight; var W = c.clientWidth, H = c.clientHeight;
@@ -169,7 +322,7 @@
var g = c.getContext('2d'); var g = c.getContext('2d');
g.setTransform(dpr,0,0,dpr,0,0); g.setTransform(dpr,0,0,dpr,0,0);
g.clearRect(0,0,W,H); g.clearRect(0,0,W,H);
g.fillStyle = '#0b1220'; g.fillRect(0,0,W,H); g.fillStyle = P.bg; g.fillRect(0,0,W,H);
var cols = d.eye.cols, rows = d.eye.rows, amp = d.eye.amp; var cols = d.eye.cols, rows = d.eye.rows, amp = d.eye.amp;
var bin = atob(d.eye.data), x, y; var bin = atob(d.eye.data), x, y;
@@ -196,30 +349,74 @@
g.drawImage(off, L, T, W - L - R, H - T - B); g.drawImage(off, L, T, W - L - R, H - T - B);
function yOfLvl(v){ return T + (amp - v) / (2 * amp) * (H - T - B); } function yOfLvl(v){ return T + (amp - v) / (2 * amp) * (H - T - B); }
g.strokeStyle = 'rgba(57,208,255,0.45)'; g.lineWidth = 0.7; g.strokeStyle = P.eyeLevel; g.lineWidth = 0.7;
[-7,-5,-3,-1,1,3,5,7].forEach(function(v){ [-7,-5,-3,-1,1,3,5,7].forEach(function(v){
var yy = yOfLvl(v); var yy = yOfLvl(v);
g.beginPath(); g.moveTo(L,yy); g.lineTo(W-R,yy); g.stroke(); g.beginPath(); g.moveTo(L,yy); g.lineTo(W-R,yy); g.stroke();
}); });
var xc = L + (W - L - R) / 2; /* decision instant = centre column */ var xc = L + (W - L - R) / 2; /* decision instant = centre column */
g.strokeStyle = '#a3e635'; g.setLineDash([4,3]); g.strokeStyle = P.decision; g.setLineDash([4,3]);
g.beginPath(); g.moveTo(xc,T); g.lineTo(xc,H-B); g.stroke(); g.beginPath(); g.moveTo(xc,T); g.lineTo(xc,H-B); g.stroke();
g.setLineDash([]); g.setLineDash([]);
g.fillStyle = '#94a3b8'; g.font = '11px sans-serif'; g.fillStyle = P.axisLbl; g.font = '11px sans-serif';
g.textAlign = 'right'; g.textBaseline = 'middle'; g.textAlign = 'right'; g.textBaseline = 'middle';
[-7,-5,-3,-1,1,3,5,7].forEach(function(v){ g.fillText(v.toFixed(0), L-6, yOfLvl(v)); }); [-7,-5,-3,-1,1,3,5,7].forEach(function(v){ g.fillText(v.toFixed(0), L-6, yOfLvl(v)); });
g.textAlign = 'center'; g.textBaseline = 'top'; g.textAlign = 'center'; g.textBaseline = 'top';
[-1,-0.5,0,0.5,1].forEach(function(u){ [-1,-0.5,0,0.5,1].forEach(function(u){
g.fillText(u.toFixed(1), L + (u+1)/2*(W-L-R), H-B+5); g.fillText(u.toFixed(1), L + (u+1)/2*(W-L-R), H-B+5);
}); });
g.fillStyle = '#64748b'; g.textBaseline = 'bottom'; g.fillStyle = P.axisTtl; g.textBaseline = 'bottom';
g.fillText('symbol periods (UI) - decision instant at 0', (L+W-R)/2, H-2); g.fillText('symbol periods (UI) - decision instant at 0', (L+W-R)/2, H-2);
g.save(); g.translate(12,(T+H-B)/2); g.rotate(-Math.PI/2); g.save(); g.translate(12,(T+H-B)/2); g.rotate(-Math.PI/2);
g.textBaseline = 'top'; g.fillText('normalised I level', 0, 0); g.restore(); g.textBaseline = 'top'; g.fillText('normalised I level', 0, 0); g.restore();
} }
/* ---- canvas ---- */ /* ---- canvas ---- */
/* -----------------------------------------------------------------------
R5 (2026-08-11): colour lives in the stylesheet, never in this file.
Read once per draw and cached until the theme class changes -- at a 1 Hz
redraw the cost is nil, and it means a family colour ruling reaches these
plots without anyone editing JavaScript again.
Each read carries the pre-R5 literal as its fallback ON PURPOSE: this
product has already shipped a unit running a STALE styles.css against a
correct package (S3, the white-banner round). If that happens again the
plots degrade to exactly today's appearance instead of to black.
----------------------------------------------------------------------- */
var PLOT = (function(){
var cache = null, cacheKey = null;
return function(){
var key = document.documentElement.className;
if (cache && cacheKey === key) return cache;
var cs = getComputedStyle(document.documentElement);
function v(name, fallback){
var got = cs.getPropertyValue(name);
got = got ? got.trim() : '';
return got || fallback;
}
cache = {
bg: v('--plot-bg', '#0b1220'),
grid: v('--plot-grid', '#1e293b'),
zero: v('--plot-grid-zero', '#334155'),
axisLbl: v('--plot-axis-label', '#94a3b8'),
axisTtl: v('--plot-axis-title', '#64748b'),
trace: v('--plot-trace', '#38bdf8'),
hold: v('--plot-trace-hold', '#f59e0b'),
marker: v('--plot-marker', '#f87171'),
band: v('--plot-band', 'rgba(56,189,248,0.10)'),
eyeLevel: v('--plot-eye-level', 'rgba(57,208,255,0.45)'),
decision: v('--plot-eye-decision', '#a3e635'),
traceError: v('--plot-trace-error', '#eab308')
};
cacheKey = key;
return cache;
};
})();
/* NOTE: the eye's density colormap (40+255v^0.75 / 20+200v^1.6 / 60+120v^2.6)
is an ALGORITHM, not a palette entry, and is left exactly as shipped. It is
the reason the plot field stays dark in both themes -- see styles.css. */
function specGeom(){ function specGeom(){
var c = document.getElementById('spec_canvas'); var c = document.getElementById('spec_canvas');
var dpr = window.devicePixelRatio || 1; var dpr = window.devicePixelRatio || 1;
@@ -235,12 +432,13 @@
function yOf(m, db){ return m.t + (Y_TOP - db)/(Y_TOP - Y_BOT) * (m.h - m.t - m.b); } function yOf(m, db){ return m.t + (Y_TOP - db)/(Y_TOP - Y_BOT) * (m.h - m.t - m.b); }
function drawGrid(fmin, fmax){ function drawGrid(fmin, fmax){
var P = PLOT();
if (fmin === undefined){ fmin = -5; fmax = 5; } if (fmin === undefined){ fmin = -5; fmax = 5; }
var m = specGeom(), g = m.g, f, db; var m = specGeom(), g = m.g, f, db;
g.clearRect(0,0,m.w,m.h); g.clearRect(0,0,m.w,m.h);
g.fillStyle = '#0b1220'; g.fillRect(0,0,m.w,m.h); g.fillStyle = P.bg; g.fillRect(0,0,m.w,m.h);
g.fillStyle = 'rgba(56,189,248,0.10)'; // the band the shoulder is measured in g.fillStyle = P.band; // the band the shoulder is measured in
[[SH_LO,SH_HI],[-SH_HI,-SH_LO]].forEach(function(b){ [[SH_LO,SH_HI],[-SH_HI,-SH_LO]].forEach(function(b){
var x0 = xOf(m,Math.max(b[0],fmin),fmin,fmax), x1 = xOf(m,Math.min(b[1],fmax),fmin,fmax); var x0 = xOf(m,Math.max(b[0],fmin),fmin,fmax), x1 = xOf(m,Math.min(b[1],fmax),fmin,fmax);
g.fillRect(x0, m.t, x1-x0, m.h-m.t-m.b); g.fillRect(x0, m.t, x1-x0, m.h-m.t-m.b);
@@ -251,18 +449,18 @@
g.textAlign = 'center'; g.textBaseline = 'top'; g.textAlign = 'center'; g.textBaseline = 'top';
for (f = Math.ceil(fmin); f <= fmax; f++){ for (f = Math.ceil(fmin); f <= fmax; f++){
var x = Math.round(xOf(m,f,fmin,fmax)) + 0.5; var x = Math.round(xOf(m,f,fmin,fmax)) + 0.5;
g.strokeStyle = '#1e293b'; g.strokeStyle = P.grid;
g.beginPath(); g.moveTo(x,m.t); g.lineTo(x,m.h-m.b); g.stroke(); g.beginPath(); g.moveTo(x,m.t); g.lineTo(x,m.h-m.b); g.stroke();
g.fillStyle = '#94a3b8'; g.fillText(f.toFixed(0), x, m.h-m.b+5); g.fillStyle = P.axisLbl; g.fillText(f.toFixed(0), x, m.h-m.b+5);
} }
g.textAlign = 'right'; g.textBaseline = 'middle'; g.textAlign = 'right'; g.textBaseline = 'middle';
for (db = Y_TOP; db >= Y_BOT; db -= 10){ for (db = Y_TOP; db >= Y_BOT; db -= 10){
var y = Math.round(yOf(m,db)) + 0.5; var y = Math.round(yOf(m,db)) + 0.5;
g.strokeStyle = (db === 0) ? '#334155' : '#1e293b'; g.strokeStyle = (db === 0) ? P.zero : P.grid;
g.beginPath(); g.moveTo(m.l,y); g.lineTo(m.w-m.r,y); g.stroke(); g.beginPath(); g.moveTo(m.l,y); g.lineTo(m.w-m.r,y); g.stroke();
g.fillStyle = '#94a3b8'; g.fillText(db.toFixed(0), m.l-6, y); g.fillStyle = P.axisLbl; g.fillText(db.toFixed(0), m.l-6, y);
} }
g.fillStyle = '#64748b'; g.textAlign = 'center'; g.textBaseline = 'bottom'; g.fillStyle = P.axisTtl; g.textAlign = 'center'; g.textBaseline = 'bottom';
g.fillText('MHz from carrier', (m.l+m.w-m.r)/2, m.h-2); g.fillText('MHz from carrier', (m.l+m.w-m.r)/2, m.h-2);
g.save(); g.translate(12,(m.t+m.h-m.b)/2); g.rotate(-Math.PI/2); g.save(); g.translate(12,(m.t+m.h-m.b)/2); g.rotate(-Math.PI/2);
g.textBaseline = 'top'; g.fillText('dB rel. in-band flat density', 0, 0); g.restore(); g.textBaseline = 'top'; g.fillText('dB rel. in-band flat density', 0, 0); g.restore();
@@ -282,23 +480,24 @@
} }
function drawSpectrum(d){ function drawSpectrum(d){
var P = PLOT();
var fmin = d.f0, fmax = d.f0 + (d.n-1)*d.df; var fmin = d.f0, fmax = d.f0 + (d.n-1)*d.df;
var m = drawGrid(fmin, fmax), i; var m = drawGrid(fmin, fmax), i;
if (elChecked('spec_hold')){ if (elChecked('spec_hold')){
if (!specPeak || specPeak.length !== d.psd.length) specPeak = d.psd.slice(); if (!specPeak || specPeak.length !== d.psd.length) specPeak = d.psd.slice();
else for (i = 0; i < d.psd.length; i++) if (d.psd[i] > specPeak[i]) specPeak[i] = d.psd[i]; else for (i = 0; i < d.psd.length; i++) if (d.psd[i] > specPeak[i]) specPeak[i] = d.psd[i];
trace(m, specPeak, d.f0, d.df, fmin, fmax, '#f59e0b', 1); trace(m, specPeak, d.f0, d.df, fmin, fmax, P.hold, 1);
} else specPeak = null; } else specPeak = null;
trace(m, d.psd, d.f0, d.df, fmin, fmax, '#38bdf8', 1.2); trace(m, d.psd, d.f0, d.df, fmin, fmax, P.trace, 1.2);
var y = yOf(m, d.shoulder); // measured shoulder level var y = yOf(m, d.shoulder); // measured shoulder level
m.g.strokeStyle = '#f87171'; m.g.setLineDash([5,4]); m.g.lineWidth = 1; m.g.strokeStyle = P.marker; m.g.setLineDash([5,4]); m.g.lineWidth = 1;
m.g.beginPath(); m.g.moveTo(m.l,y); m.g.lineTo(m.w-m.r,y); m.g.stroke(); m.g.beginPath(); m.g.moveTo(m.l,y); m.g.lineTo(m.w-m.r,y); m.g.stroke();
m.g.setLineDash([]); m.g.setLineDash([]);
m.g.fillStyle = '#f87171'; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom'; m.g.fillStyle = P.marker; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
m.g.fillText('shoulder ' + d.shoulder.toFixed(1) + ' dB', m.l+6, y-3); m.g.fillText('shoulder ' + (d.shoulder - 11).toFixed(1) + ' dBc', m.l+6, y-3);
/* PR12: pilot vertical marker removed on request (it covered the pilot /* PR12: pilot vertical marker removed on request (it covered the pilot
* peak). The pilot frequency is still available in d.pilot. */ * peak). The pilot frequency is still available in d.pilot. */
@@ -311,7 +510,7 @@
/* DISABLED on request (2026-07-25): drop the leading `false &&` to restore */ /* DISABLED on request (2026-07-25): drop the leading `false &&` to restore */
if (false && eyeLastData && eyeLastData.err && eyeLastData.err.db){ if (false && eyeLastData && eyeLastData.err && eyeLastData.err.db){
var er = eyeLastData.err, g2 = m.g, started = false, ei; var er = eyeLastData.err, g2 = m.g, started = false, ei;
g2.strokeStyle = '#eab308'; g2.lineWidth = 1.4; g2.beginPath(); g2.strokeStyle = P.traceError; g2.lineWidth = 1.4; g2.beginPath();
for (ei = 0; ei < er.db.length; ei++){ for (ei = 0; ei < er.db.length; ei++){
var ef = er.f0 + ei*er.df; var ef = er.f0 + ei*er.df;
if (ef < fmin || ef > fmax){ started = false; continue; } if (ef < fmin || ef > fmax){ started = false; continue; }
@@ -498,12 +697,13 @@
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li> <li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li class="active"><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li class="active"><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li> <li><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -523,7 +723,14 @@
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -544,6 +751,7 @@
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -570,16 +778,6 @@
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -590,6 +788,16 @@
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
@@ -670,27 +878,62 @@
<main class="app-main"> <main class="app-main">
<h2>ORx Receive Spectrum</h2> <div class="spec-tabs" role="tablist" aria-label="Spectrum display">
<div class="box"> <button type="button" class="spec-tab active" id="spectrum_tab" role="tab" aria-selected="true" onclick="selectSpectrumView('spectrum')">Output Spectrum</button>
<!-- PR12: Refresh/Pause/Peak-hold toolbar + status line removed on <button type="button" class="spec-tab" id="eye_tab" role="tab" aria-selected="false" onclick="selectSpectrumView('eye')">8VSB Eye Diagram</button>
request (fixed 1 s refresh). Readout cards were removed earlier;
the on-plot shoulder line and yellow error trace remain. -->
<canvas id="spec_canvas"></canvas>
</div> </div>
<h2>8VSB Eye Diagram</h2> <section class="spectrum-panel" id="spectrum_panel" role="tabpanel" aria-labelledby="spectrum_tab">
<div class="box"> <div class="box">
<div class="fssi-refresh-notice">FSSI Refresh is ON. Spectrum and Eye Diagram are suspended.</div>
<div class="spec-toolbar">
<label>Refresh
<select id="spec_rate" onchange="spectrumControlsChanged()">
<option value="500">0.5 s</option>
<option value="1000" selected>1 s</option>
<option value="2000">2 s</option>
<option value="5000">5 s</option>
</select>
</label>
<label><input type="checkbox" id="spec_pause" onchange="spectrumControlsChanged()"> Pause</label>
<label><input type="checkbox" id="spec_hold" onchange="peakHoldChanged()"> Peak hold</label>
<span id="spec_state">starting&hellip;</span>
</div>
<canvas id="spec_canvas"></canvas>
<div class="spec-readouts">
<div class="spec-readout" title="ORx front-end signal strength from the FAGC gain index. A path; A / B when the FSSI refresh switch is on."><span class="k">FSSI</span><span class="v"><span id="rd_fssi">--</span> %</span></div>
<div class="spec-readout" title="Shoulder level, dBc (signed; includes the −11 dB pilot offset)."><span class="k">Shoulder</span><span class="v"><span id="rd_shoulder">--</span> dBc</span></div>
<div class="spec-readout-pair">
<div class="spec-readout" title="Lower shoulder IMD, dBc (includes the +11 dB pilot offset; matches the dashboard LIMD)."><span class="k">LIMD</span><span class="v"><span id="rd_limd">--</span> dBc</span></div>
<div class="spec-readout" title="Upper shoulder IMD, dBc (includes the +11 dB pilot offset; matches the dashboard UIMD)."><span class="k">UIMD</span><span class="v"><span id="rd_uimd">--</span> dBc</span></div>
</div>
<div class="spec-readout"><span class="k">Pilot</span><span class="v"><span id="rd_pilot">--</span> MHz</span></div>
<div class="spec-readout"><span class="k">Peak</span><span class="v"><span id="rd_peak">--</span> dB</span></div>
<div class="spec-readout"><span class="k">RBW</span><span class="v"><span id="rd_rbw">--</span> kHz</span></div>
<div class="spec-readout"><span class="k">Packets</span><span class="v" id="rd_pkts">--</span></div>
<div class="spec-readout"><span class="k">Contiguous</span><span class="v"><span id="rd_ms">--</span> ms</span></div>
<div class="spec-readout"><span class="k">&gt; 1 field</span><span class="v" id="rd_field">--</span></div>
</div>
</div>
</section>
<section class="spectrum-panel" id="eye_panel" role="tabpanel" aria-labelledby="eye_tab" hidden>
<div class="box">
<div class="fssi-refresh-notice">FSSI Refresh is ON. Spectrum and Eye Diagram are suspended.</div>
<!-- PR12: eye caption + status line removed on request. --> <!-- PR12: eye caption + status line removed on request. -->
<canvas id="eye_canvas"></canvas> <canvas id="eye_canvas"></canvas>
<div class="spec-readouts"> <div class="spec-readouts">
<div class="spec-readout" title="MER, I-path definition: 10log10(sum d^2 / sum (r-d)^2)"><span class="k">SNR / MER</span><span class="v"><span id="rd_snr">--</span> dB</span></div> <div class="spec-readout" title="MER, I-path definition: 10log10(sum d^2 / sum (r-d)^2)"><span class="k">SNR / MER</span><span class="v"><span id="rd_snr">--</span> dB</span></div>
<div class="spec-readout" title="MER before the ORx AGC gain steps are tracked out. The gap to SNR/MER is what the observation receiver costs you, not the transmitter."><span class="k">SNR raw</span><span class="v"><span id="rd_snrraw">--</span> dB</span></div> <div class="spec-readout" title="Smallest of the 7 eye openings at the decision instant."><span class="k">Min eye height</span><span class="v" id="rd_eyeh">--</span></div>
<!-- PR13: FSSI from the ORx FAGC gain index (255->0%, 195->100%). <div class="spec-readout" title="Time span over which the central eye stays open."><span class="k">Eye width</span><span class="v"><span id="rd_eyew">--</span> UI</span></div>
Value comes from the shared 3 s dashboard poll (dashboard.js), <div class="spec-readout" title="Sampling phase offset."><span class="k">&tau;0</span><span class="v"><span id="rd_tau">--</span> sa</span></div>
not from iq_eye.cgi. Shows "A / B" when FSSI refresh is on. --> <div class="spec-readout" title="Segment synchronization correlation ratio."><span class="k">Seg sync</span><span class="v" id="rd_segsync">--</span></div>
<div class="spec-readout" title="ORx front-end signal strength from the FAGC gain index (gain index 255 -> 0%, 195 -> 100%). A path; A / B when the FSSI refresh switch is on."><span class="k">FSSI</span><span class="v"><span id="rd_fssi">--</span> %</span></div> <div class="spec-readout" title="Pilot phase drift across the window."><span class="k">Carrier offset</span><span class="v"><span id="rd_coff">--</span> Hz</span></div>
<div class="spec-readout" title="MER before ORx AGC gain steps are tracked out."><span class="k">SNR raw</span><span class="v"><span id="rd_snrraw">--</span> dB</span></div>
<div class="spec-readout" title="RMS of ORx RF AGC gain steps over the window."><span class="k">ORx AGC jitter</span><span class="v"><span id="rd_gjit">--</span> %</span></div>
</div> </div>
</div> </div>
</section>
</main> </main>
</div> </div>
+95 -29
View File
@@ -4,7 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title> <title id="webtitle"></title>
<link href="css/styles.css" rel="stylesheet" media="screen"> <script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script> <script src="js/auth.js"></script>
<script src="js/jquery.js"></script> <script src="js/jquery.js"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
@@ -58,10 +59,66 @@
var table = data.split("|"); var table = data.split("|");
g_remot_local_ctrl = Number(table[0].split("#")[5]); 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. */
}); });
} }
/* ==========================================================================
R4 (2026-08-11, owner ruling) — NO UNNAMED VALUE MAY BE POSTED
==========================================================================
Protocol / Length / Type each carried a third option, "Invalid", and the page
clamped any out-of-range firmware value into it (if(i >= 2) i = 2). The owner
challenged the premise and was right: these are SETTINGS. The write path posts
the raw select value (tsoip_setcfg, "#"+forms.pktp.value), so an operator who
pressed Set while a control sat on "Invalid" would send 2 back to the unit — a
value this page cannot name. Disabling the option would NOT have closed that:
a disabled option can still be the selected one and .value still reads 2.
So the option is deleted, and the read path is made honest instead: a value we
recognise selects its option; anything else leaves the control UNSET, states the
reason, and holds this section's Set until the operator picks a real value. The
guard only ever blocks; it releases as soon as all three resolve (and never in
Local mode, where the button is locked for a different reason). */
var TS_ENUM_IDS = ['pktp', 'pklen', 'pkrec'];
function setEnumFromUnit(sel, i) {
if (!sel) return false;
if (i >= 0 && i < sel.options.length) { sel.selectedIndex = i; return true; }
sel.selectedIndex = -1; // no phantom slot to land on
return false;
}
function tsEnumsResolved() {
for (var k = 0; k < TS_ENUM_IDS.length; k++) {
var sel = document.getElementById(TS_ENUM_IDS[k]);
if (!sel || sel.selectedIndex < 0) return false;
}
return true;
}
function applyTsEnumGuard() {
var ok = tsEnumsResolved();
var note = document.getElementById('ts_enum_note');
var btn = document.getElementById('setcfg_id1');
if (note) note.style.display = ok ? 'none' : 'block';
if (!btn) return;
if (!ok) { btn.disabled = true; }
else if (g_remot_local_ctrl != 1) { btn.disabled = false; }
}
function attachTsEnumGuard() {
for (var k = 0; k < TS_ENUM_IDS.length; k++) {
var sel = document.getElementById(TS_ENUM_IDS[k]);
if (sel) sel.addEventListener('change', applyTsEnumGuard);
}
applyTsEnumGuard();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', attachTsEnumGuard);
} else {
attachTsEnumGuard();
}
function SetFormDefaults() { function SetFormDefaults() {
// 1. DISABLE BUTTONS START (only in Remote mode) // 1. DISABLE BUTTONS START (only in Remote mode)
if(g_remot_local_ctrl == 1) { if(g_remot_local_ctrl == 1) {
@@ -150,17 +207,11 @@
var lpk_para = Number(tb_systs[ops]); var lpk_para = Number(tb_systs[ops]);
ops++; ops++;
i = (lpk_para&0xff0000)>>16; /* R4 (2026-08-11): the three clamps that parked an unrecognised firmware value on
if(i >= 2) i = 2; a phantom "Invalid" option are gone, and so is the option. See setEnumFromUnit. */
fcfg1.pktp.selectedIndex = i; setEnumFromUnit(fcfg1.pktp, (lpk_para&0xff0000)>>16);
setEnumFromUnit(fcfg1.pklen, (lpk_para&0x00ff00)>>8);
i = (lpk_para&0x00ff00)>>8; setEnumFromUnit(fcfg1.pkrec, (lpk_para&0x0000ff));
if(i >= 2) i = 2;
fcfg1.pklen.selectedIndex = i;
i = (lpk_para&0x0000ff);
if(i >= 2) i = 2;
fcfg1.pkrec.selectedIndex = i;
fcfg1.fec_swh.selectedIndex=Number(tb_systs[ops]); fcfg1.fec_swh.selectedIndex=Number(tb_systs[ops]);
ops++; ops++;
@@ -186,6 +237,7 @@
// Remote mode: enable button // Remote mode: enable button
$('#setnet_id_1, #setcfg_id1').attr("disabled", false).val("Set"); $('#setnet_id_1, #setcfg_id1').attr("disabled", false).val("Set");
} }
applyTsEnumGuard(); // R4: re-assert after every readback — only ever blocks
if (typeof setFormSnapshotUpdate === 'function') setFormSnapshotUpdate(); if (typeof setFormSnapshotUpdate === 'function') setFormSnapshotUpdate();
@@ -514,12 +566,13 @@ function cmd_confirm() {
<div class="app-layout"> <div class="app-layout">
<aside class="app-sidebar"> <aside class="app-sidebar">
<ul class="items"> <ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li> <li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li> <li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li> <li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li> <li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li class="active"><a href="sys_ts.html" title="TSoIP">TSoIP</a></li> <li class="active"><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li> <li><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li> <li><a href="log_output.html" title="Log">Log</a></li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -539,7 +592,14 @@ function cmd_confirm() {
</button> </button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1> <h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2> <h2 id="webtitle_header"><span></span></h2>
<h3> <h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button> <button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content"> <div class="dropdown-content">
@@ -560,6 +620,7 @@ function cmd_confirm() {
</a> </a>
</div> </div>
</div> </div>
</div>
</h3> </h3>
</header> </header>
@@ -586,16 +647,6 @@ function cmd_confirm() {
<span class="summary-unit">dB</span> <span class="summary-unit">dB</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span> <span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -606,6 +657,16 @@ function cmd_confirm() {
<span class="summary-unit">dBc</span> <span class="summary-unit">dBc</span>
</div> </div>
</div> </div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-label-row"> <div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span> <span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
@@ -720,7 +781,7 @@ function cmd_confirm() {
</tr> </tr>
<tr style="display: none;"> <tr style="display: none;">
<td>NET SPEED</td> <td>Net Speed</td>
<td> <td>
<select name="eth_spd" id="eth_spd"> <select name="eth_spd" id="eth_spd">
<option value="0">100Mbps <option value="0">100Mbps
@@ -762,7 +823,6 @@ function cmd_confirm() {
<select name="pktp" id="pktp"> <select name="pktp" id="pktp">
<option value="0">UDP <option value="0">UDP
<option value="1">RTP <option value="1">RTP
<option value="2">Invalid
</select> </select>
</td> </td>
</tr> </tr>
@@ -772,7 +832,6 @@ function cmd_confirm() {
<select name="pklen" id="pklen"> <select name="pklen" id="pklen">
<option value="0">188 <option value="0">188
<option value="1">204 <option value="1">204
<option value="2">Invalid
</select> </select>
</td> </td>
<td>Type</td> <td>Type</td>
@@ -780,7 +839,6 @@ function cmd_confirm() {
<select name="pkrec" id="pkrec"> <select name="pkrec" id="pkrec">
<option value="0">Multicast <option value="0">Multicast
<option value="1">Unicast <option value="1">Unicast
<option value="2">Invalid
</select> </select>
</td> </td>
</tr> </tr>
@@ -813,6 +871,14 @@ function cmd_confirm() {
<td> <input type="button" class="btn-modern" alt="Set" value="Set" disabled id="setcfg_id1" onclick="return tsoip_setcfg(0);"> </td> <td> <input type="button" class="btn-modern" alt="Set" value="Set" disabled id="setcfg_id1" onclick="return tsoip_setcfg(0);"> </td>
</tr> </tr>
</table> </table>
<!-- R4 (2026-08-11): shown only when the unit reports a Protocol/Length/Type
value this page has no name for. Replaces the old "Invalid" option. -->
<!-- R6: invented .field-note.is-error dropped. Unrecognised value is a genuine
section-level fault -> .warning-banner.is-error (red), per the 3.0 scope rule. -->
<div class="warning-banner is-error" id="ts_enum_note" style="display:none">
The unit reported a Protocol, Length or Type value this page does not recognise.
That control has been left unset and Set is held until a valid value is chosen.
</div>
</form> </form>
</div> </div>
</main> </main>