新增:概览仪表盘及统一频谱页面
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
/* 受保护的 HTML 页面列表 */
|
||||
static const char *protected_pages[] = {
|
||||
"overview.html",
|
||||
"status_info.html",
|
||||
"sys_set.html",
|
||||
"sys_rf.html",
|
||||
@@ -43,7 +44,7 @@ static const char* get_requested_page(const char *query_string) {
|
||||
static char page_name[256] = {0};
|
||||
|
||||
if (!query_string || strlen(query_string) == 0) {
|
||||
return "status_info.html"; /* 默认页面 */
|
||||
return "overview.html"; /* 默认页面 */
|
||||
}
|
||||
|
||||
/* 解析 query string 中的 page=xxx 参数 */
|
||||
@@ -57,7 +58,7 @@ static const char* get_requested_page(const char *query_string) {
|
||||
return page_name;
|
||||
}
|
||||
|
||||
return "status_info.html";
|
||||
return "overview.html";
|
||||
}
|
||||
|
||||
/* 发送重定向响应 */
|
||||
@@ -130,7 +131,7 @@ int main(int argc, char **argv) {
|
||||
if (cookie && extract_cookie(cookie, SESSION_COOKIE_NAME,
|
||||
session_id, sizeof(session_id))) {
|
||||
if (validate_session(session_id, NULL, 0)) {
|
||||
send_redirect("status_info.html");
|
||||
send_redirect("overview.html");
|
||||
}
|
||||
}
|
||||
/* 否则返回登录页 */
|
||||
|
||||
@@ -51,6 +51,29 @@ int simplified_create_client_socket(int port)
|
||||
return sockfd;
|
||||
}
|
||||
|
||||
/* Read an application value for a public login-page endpoint. */
|
||||
static int simplified_get_data_from_process(int modulePort, int moduleKey, char *buf, size_t bufLen)
|
||||
{
|
||||
cgi_process_elem_t elem;
|
||||
int fd;
|
||||
|
||||
if(!buf || bufLen == 0 || modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE)
|
||||
return -1;
|
||||
memset(&elem, 0, sizeof(elem));
|
||||
elem.method = GET_METHOD;
|
||||
elem.keyType = moduleKey;
|
||||
fd = simplified_create_client_socket(modulePort);
|
||||
if(fd < 0)
|
||||
return -1;
|
||||
if(write(fd, &elem, sizeof(elem)) != sizeof(elem) || read(fd, &elem, sizeof(elem)) != sizeof(elem)){
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
snprintf(buf, bufLen, "%s", elem.data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Define simplified version of set_data_from_process to avoid linking issues */
|
||||
void simplified_set_data_from_process(int modulePort, int moduleKey, char *buf) {
|
||||
if(modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE)
|
||||
@@ -398,7 +421,7 @@ int main(int argc, char **argv) {
|
||||
sizeof(cookie_header), 0);
|
||||
printf("Content-type: text/html\r\n");
|
||||
printf("%s\r\n", cookie_header);
|
||||
printf("Location: status_info.html\r\n\r\n");
|
||||
printf("Location: overview.html\r\n\r\n");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
@@ -412,6 +435,16 @@ int main(int argc, char **argv) {
|
||||
|
||||
/* === Handle GET request - get current username === */
|
||||
if (method && strcasecmp(method, "GET") == 0 && query) {
|
||||
/* Public read-only title used before a session exists on login.html. */
|
||||
if (strstr(query, "action=get_title")) {
|
||||
char title[MSG_DATA_LEN] = {0};
|
||||
printf("Content-type: text/plain\r\nCache-Control: no-store\r\n\r\n");
|
||||
if(simplified_get_data_from_process(WEB_SOCKET_PORT, CGIKEY_WEB_TITLE,
|
||||
title, sizeof(title)) == 0)
|
||||
printf("%s", title);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/* Check for get_user action */
|
||||
if (strstr(query, "action=get_user")) {
|
||||
char *cookie = getenv("HTTP_COOKIE");
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#define USER_WEB_INDEX_HTML "status_info.html"
|
||||
#define USER_WEB_INDEX_HTML "overview.html"
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
+631
-77
@@ -7,7 +7,7 @@
|
||||
--accent-green-bright: #4ade80;
|
||||
--accent-green-dim: #16a34a;
|
||||
/* Error / alert */
|
||||
--status-err: #ef4444;
|
||||
--status-err: #ff5252;
|
||||
--status-err-dim: #dc2626;
|
||||
/* Warning / amber */
|
||||
--status-warn: #f59e0b;
|
||||
@@ -23,15 +23,54 @@
|
||||
/* Text */
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--text-muted: #b0b8c4;
|
||||
--plp-pill-bg: #3a4452;
|
||||
--chart-inset: #2a313c;
|
||||
/* Informational text/labels (status notes, progress labels). Light value set in .light-theme. */
|
||||
--info-text: var(--status-neutral);
|
||||
/* Mode identity colours (overview Signal Chain page only). Light values set in .light-theme.
|
||||
Gateway intentionally reuses --accent-green / --accent-green-bright (brand). */
|
||||
--mode-exciter: #60a5fa; --mode-exciter-hover: #93bbfd;
|
||||
--mode-translator: #fb923c; --mode-translator-hover: #fdba74;
|
||||
--mode-standalone: #a78bfa; --mode-standalone-hover: #c4b5fd;
|
||||
--mode-exciter-tint: rgba(96,165,250,0.10);
|
||||
--mode-translator-tint: rgba(251,146,60,0.10);
|
||||
--mode-standalone-tint: rgba(167,139,250,0.10);
|
||||
--mode-gateway-tint: rgba(34,197,94,0.10);
|
||||
/* Stronger (hover) variants of the mode tints. */
|
||||
--mode-exciter-tint-strong: rgba(96,165,250,0.22);
|
||||
--mode-translator-tint-strong: rgba(251,146,60,0.22);
|
||||
--mode-standalone-tint-strong: rgba(167,139,250,0.22);
|
||||
--mode-gateway-tint-strong: rgba(34,197,94,0.22);
|
||||
/* Borders */
|
||||
--border-subtle: #4b5563;
|
||||
--border-accent: var(--accent-green);
|
||||
/* Sidebar */
|
||||
/* Sidebar (DARK ISLAND — sidebar stays dark navy in BOTH themes, so these
|
||||
tokens are NOT overridden in .light-theme; see the .light-theme block). */
|
||||
--sidebar-bg: #0f172a;
|
||||
--sidebar-text: #94a3b8;
|
||||
--sidebar-text-active: #ffffff;
|
||||
--sidebar-hover: #1e293b;
|
||||
/* Active-item accent. Lives as its own token (not --accent-green) because the
|
||||
sidebar is a dark island: this MUST stay bright green in light theme, whereas
|
||||
--accent-green darkens. Same value as --accent-green in dark theme. */
|
||||
--sidebar-accent: #22c55e;
|
||||
/* Primary-action button family (theme-aware). Dark values = today's look,
|
||||
so dark-only products are visually unchanged. Light values softened
|
||||
(entry 011) so buttons sit in harmony on the light page, not as heavy
|
||||
dark blocks. Status dots keep --accent-green and stay vivid. */
|
||||
--btn-green-bg: var(--accent-green-dim);
|
||||
--btn-green-bg-hover: var(--accent-green);
|
||||
--btn-green-text: #06230f;
|
||||
--btn-green-text-hover: #06230f;
|
||||
--btn-neutral-bg: #4b5563;
|
||||
--btn-neutral-text: #ffffff;
|
||||
/* Sidebar footer/labels: PINNED readable colours (sidebar is a dark island,
|
||||
so these must NOT follow the theme tokens, which flip dark in light mode). */
|
||||
--sidebar-footer-h: #cbd5e1;
|
||||
--sidebar-footer-p: #94a3b8;
|
||||
--sidebar-footer-link: #7fb3d5;
|
||||
--sidebar-label: #7c8595;
|
||||
/* Legacy / UI */
|
||||
--primary: var(--accent-green);
|
||||
--primary-hover: var(--accent-green-dim);
|
||||
@@ -41,6 +80,31 @@
|
||||
--sidebar-width: 220px;
|
||||
--header-height: 64px;
|
||||
--dashboard-height: auto;
|
||||
|
||||
/* ===== PLOT PALETTE (family, added 2026-08-11) ==============================
|
||||
Colours for the spectrum + eye canvases on the Measurements page. Present as
|
||||
DORMANT family tokens: only smod renders a Measurements page today, but the
|
||||
page is planned for other products, so the tokens live in the shared sheet
|
||||
rather than being hand-copied later (the exact mistake that motivated this
|
||||
whole stylesheet consolidation). Values are the instrument-plot palette; four
|
||||
of them already equal existing family tokens (trace=--status-neutral,
|
||||
hold=--status-warn, band=neutral@10%, eye-level near neutral) and are kept as
|
||||
separate names so a family colour change cannot silently restyle a scope plot.
|
||||
The plot field stays dark in BOTH themes on purpose: the eye is a density map
|
||||
whose floor must read as "no signal", which a dark field gives and a light one
|
||||
does not. Not overridden in .light-theme, deliberately. =================== */
|
||||
--plot-bg: #0b1220;
|
||||
--plot-grid: #1e293b;
|
||||
--plot-grid-zero: #334155;
|
||||
--plot-axis-label: #94a3b8;
|
||||
--plot-axis-title: #64748b;
|
||||
--plot-trace: #38bdf8;
|
||||
--plot-trace-hold: #f59e0b;
|
||||
--plot-trace-error: #eab308;
|
||||
--plot-marker: #f87171;
|
||||
--plot-band: rgba(56, 189, 248, 0.10);
|
||||
--plot-eye-level: rgba(57, 208, 255, 0.45);
|
||||
--plot-eye-decision: #a3e635;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -165,20 +229,41 @@ body {
|
||||
.app-sidebar li.active a {
|
||||
background: transparent;
|
||||
color: var(--sidebar-text-active);
|
||||
border-left: 3px solid var(--accent-green);
|
||||
border-left: 3px solid var(--sidebar-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --- Sidebar group labels (Operations / Configuration / Admin) ---
|
||||
Added for ATSC 3.0 pages. Scoped by the .app-sidebar ancestor plus
|
||||
the .ov-group-label class, so these rules only affect <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 {
|
||||
padding: 16px;
|
||||
color: var(--text-muted);
|
||||
color: var(--sidebar-footer-p);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.sidebar-footer h4 {
|
||||
color: var(--text-secondary);
|
||||
color: var(--sidebar-footer-h);
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -192,11 +277,11 @@ body {
|
||||
|
||||
.sidebar-footer .redbox-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
color: var(--sidebar-footer-h);
|
||||
}
|
||||
|
||||
.sidebar-footer a {
|
||||
color: var(--status-neutral);
|
||||
color: var(--sidebar-footer-link);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -537,8 +622,8 @@ body {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
background-color: var(--accent-green);
|
||||
color: #fff;
|
||||
background-color: var(--btn-green-bg);
|
||||
color: var(--btn-green-text-hover);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
margin-left: auto;
|
||||
@@ -547,7 +632,9 @@ body {
|
||||
}
|
||||
|
||||
.box-header.collapsed .toggle-text {
|
||||
background-color: var(--text-muted);
|
||||
background-color: var(--btn-neutral-bg);
|
||||
color: var(--btn-neutral-text);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
/* Accordion Specific Settings */
|
||||
@@ -773,8 +860,8 @@ select.display-only-select:disabled.state-ok {
|
||||
/* Buttons */
|
||||
input[type="button"].btn-modern,
|
||||
input[type="submit"].btn-modern {
|
||||
background-color: var(--accent-green-dim);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--btn-green-bg);
|
||||
color: var(--btn-green-text);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 8px 18px;
|
||||
@@ -788,14 +875,14 @@ input[type="submit"].btn-modern {
|
||||
|
||||
input[type="button"].btn-modern:hover,
|
||||
input[type="submit"].btn-modern:hover {
|
||||
background-color: var(--accent-green);
|
||||
border-color: var(--accent-green);
|
||||
color: #fff;
|
||||
background-color: var(--btn-green-bg-hover);
|
||||
border-color: var(--btn-green-bg-hover);
|
||||
color: var(--btn-green-text-hover);
|
||||
}
|
||||
|
||||
input[type="button"].btn-modern:active,
|
||||
input[type="submit"].btn-modern:active {
|
||||
background-color: var(--accent-green-dim);
|
||||
background-color: var(--btn-green-bg);
|
||||
}
|
||||
|
||||
input[type="button"].btn-modern:disabled,
|
||||
@@ -809,12 +896,26 @@ input[type="submit"].btn-modern:disabled {
|
||||
/* Unsaved reminder */
|
||||
input[type="button"].btn-modern.is-unsaved:not(:disabled),
|
||||
input[type="submit"].btn-modern.is-unsaved:not(:disabled) {
|
||||
background-color: var(--accent-green) !important;
|
||||
border-color: var(--accent-green) !important;
|
||||
color: #fff !important;
|
||||
background-color: var(--btn-green-bg) !important;
|
||||
border-color: var(--btn-green-bg-hover) !important;
|
||||
color: var(--btn-green-text-hover) !important;
|
||||
box-shadow: 0 0 10px rgba(34, 197, 94, 0.35);
|
||||
}
|
||||
|
||||
/* Cancel / secondary modal button — neutral, theme-aware (replaces hardcoded #64748b). */
|
||||
input[type="button"].btn-modern.btn-cancel,
|
||||
input[type="submit"].btn-modern.btn-cancel {
|
||||
background-color: var(--btn-neutral-bg);
|
||||
color: var(--btn-neutral-text);
|
||||
border-color: var(--border-subtle);
|
||||
}
|
||||
input[type="button"].btn-modern.btn-cancel:hover,
|
||||
input[type="submit"].btn-modern.btn-cancel:hover {
|
||||
background-color: var(--btn-neutral-bg);
|
||||
border-color: var(--text-muted);
|
||||
color: var(--btn-neutral-text);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
display: flex;
|
||||
@@ -1073,9 +1174,9 @@ BUTTON VIEW DETAILS
|
||||
}
|
||||
|
||||
.btn-view-details:hover {
|
||||
background-color: var(--accent-green);
|
||||
border-color: var(--accent-green);
|
||||
color: #fff;
|
||||
background-color: var(--btn-green-bg-hover);
|
||||
border-color: var(--btn-green-bg-hover);
|
||||
color: var(--btn-green-text-hover);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -1372,12 +1473,12 @@ BUTTON VIEW DETAILS
|
||||
|
||||
/* PLP row layout — same style as subframe-header: label | bar | bitrate | pct */
|
||||
.plp-row {
|
||||
display: grid;
|
||||
grid-template-columns: 42px 1fr auto auto;
|
||||
gap: 2px 10px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(75, 85, 99, 0.4);
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr auto auto auto;
|
||||
gap: 2px 10px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(75, 85, 99, 0.4);
|
||||
}
|
||||
|
||||
.plp-row:last-of-type {
|
||||
@@ -1395,7 +1496,7 @@ BUTTON VIEW DETAILS
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
border-radius: 4px;
|
||||
background: rgba(75, 85, 99, 0.3);
|
||||
background: var(--plp-pill-bg);
|
||||
}
|
||||
|
||||
/* Bar track (background) */
|
||||
@@ -1465,20 +1566,13 @@ BUTTON VIEW DETAILS
|
||||
.plp-pct-text.pct-red { color: var(--status-err); }
|
||||
.plp-pct-text.pct-muted { color: var(--text-muted); }
|
||||
|
||||
/* Detail line (services + FULL indicator, below bar) */
|
||||
.plp-detail {
|
||||
grid-column: 2 / -1;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* FULL indicator — beside pct text in the same row */
|
||||
.plp-full-indicator {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--status-err);
|
||||
grid-column: 5;
|
||||
grid-row: 1;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--status-err);
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
padding: 1px 7px;
|
||||
@@ -1488,32 +1582,53 @@ BUTTON VIEW DETAILS
|
||||
}
|
||||
|
||||
.plp-full-indicator.visible {
|
||||
display: inline-block;
|
||||
animation: alarmPulse 1.5s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
animation: alarmPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.plp-services {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--status-neutral);
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
/* PLP trend sparkline container — inline chart below each utilization bar.
|
||||
JS renderer writes SVG into this container on each 2-sec poll.
|
||||
Placed in grid row 2 (row 1 = bar + pct + FULL indicator) spanning the bar columns.
|
||||
Services displayed as left sidebar panel inside the SVG when present. */
|
||||
.plp-trend-spark {
|
||||
grid-column: 2 / -1;
|
||||
grid-row: 2;
|
||||
margin-top: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.plp-services:empty {
|
||||
display: none;
|
||||
/* Hide trend container when PLP row is hidden */
|
||||
.plp-row[style*="display: none"] .plp-trend-spark,
|
||||
.plp-row[style*="display:none"] .plp-trend-spark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plp-services::before {
|
||||
content: '[';
|
||||
color: var(--border-subtle);
|
||||
/* Sparkline SVG element — fills container width.
|
||||
ViewBox 700×120 with preserveAspectRatio="none" — stretches to match bar width.
|
||||
When services exist, left margin widens (ML += svcBandW) to accommodate sidebar panel. */
|
||||
.plp-trend-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
/* height set dynamically by JS via inline style — matches viewBox ratio */
|
||||
}
|
||||
|
||||
.plp-services::after {
|
||||
content: ']';
|
||||
color: var(--border-subtle);
|
||||
/* Max/min envelope band — semi-transparent green fill (set inline by JS). */
|
||||
.plp-trend-svg polygon {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
/* Subframe header trend — matches bar height */
|
||||
.subframe-header .plp-trend-spark {
|
||||
grid-column: 2 / -1;
|
||||
grid-row: 2;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Global header trend — extra tall for emphasis */
|
||||
.global-header .plp-trend-spark {
|
||||
grid-column: 2 / -1;
|
||||
grid-row: 2;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Subframe header bar (total payload at top) */
|
||||
@@ -1589,22 +1704,16 @@ BUTTON VIEW DETAILS
|
||||
|
||||
/* Responsive adjustments for utilization bars */
|
||||
@media (max-width: 767px) {
|
||||
.plp-row {
|
||||
grid-template-columns: 36px 1fr auto;
|
||||
}
|
||||
.plp-row {
|
||||
grid-template-columns: 48px 1fr auto auto;
|
||||
}
|
||||
.plp-row .plp-bitrate-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
.plp-pct-text {
|
||||
display: none;
|
||||
}
|
||||
.plp-detail {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.plp-services {
|
||||
max-width: none;
|
||||
}
|
||||
.subframe-header {
|
||||
.subframe-header {
|
||||
grid-template-columns: 48px 1fr;
|
||||
}
|
||||
.subframe-header .plp-bitrate-text {
|
||||
@@ -1615,12 +1724,12 @@ BUTTON VIEW DETAILS
|
||||
.global-header {
|
||||
grid-template-columns: 48px 1fr;
|
||||
}
|
||||
.global-header .plp-bitrate-text {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
.global-header .plp-bitrate-text {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SESSION TIMEOUT WARNING
|
||||
@@ -1694,3 +1803,448 @@ SESSION TIMEOUT WARNING
|
||||
.session-timeout-actions .btn-modern {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Sprint 1 — dashboard bar mode-adaptive visibility + group separators
|
||||
Additive only. No existing rule, selector, or variable is modified.
|
||||
Shared with 2D and exciter UIs — these classes are new and will only apply
|
||||
where the new data-tile attribute is present on summary-card elements.
|
||||
============================================================================= */
|
||||
|
||||
/* Marker class for tile-hidden state (reserved; current implementation uses
|
||||
inline style="display:none" set by applyBarTileVisibility for idempotency.
|
||||
Class kept for future use: allow CSS-only hiding via toggled class if we
|
||||
move to a transition/animation approach later.) */
|
||||
.summary-card[data-tile].tile-hidden { display: none; }
|
||||
|
||||
/* Group-separator spacing for the bar.
|
||||
The bar is a flex row; group separation is a slightly larger left margin
|
||||
on the first tile of each group (applied via a data-group-first attribute
|
||||
that pages set on select tiles).
|
||||
Purely additive. Tiles without data-group-first are unaffected. */
|
||||
.summary-card[data-group-first="input"] { margin-left: 8px; }
|
||||
.summary-card[data-group-first="output"] { margin-left: 8px; }
|
||||
.summary-card[data-group-first="housekeeping"] { margin-left: 8px; }
|
||||
|
||||
/* =============================================================================
|
||||
Sprint 1 Increment 6 — Live Trends panel (Overview page only)
|
||||
Additive only. No existing rule, selector, or variable is modified.
|
||||
These classes are new (ov-trend-*); they only apply to the new panel
|
||||
container on Overview and do not affect the shared 2D / exciter UI.
|
||||
============================================================================= */
|
||||
|
||||
/* Panel container — visual weight matches the existing ov-chain-card. */
|
||||
.ov-trend-panel {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.ov-trend-panel:empty { display: none; }
|
||||
|
||||
/* Header row — small caps, dot, title, buffer-age label. */
|
||||
.ov-trend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ov-trend-header .ov-trend-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--status-ok, #22c55e);
|
||||
box-shadow: 0 0 4px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.ov-trend-header .ov-trend-title {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ov-trend-header .ov-trend-sep {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ov-trend-header .ov-trend-age {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Grid of per-metric cards. Uses flex so cards auto-size to available
|
||||
width, wrapping to a new row only when needed. */
|
||||
.ov-trend-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ov-trend-card {
|
||||
flex: 1 1 200px;
|
||||
min-width: 180px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.ov-trend-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ov-trend-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ov-trend-num {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ov-trend-unit {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
/* No text-transform: units carry meaningful case (Mbps / dB / dBm /
|
||||
dBc / MHz / %). Matches the signal chain's .ov-blk-primary .ov-u
|
||||
convention; avoids collapsing dB / dBm / dBc distinctions into the
|
||||
same upper-cased suffix. */
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* The sparkline itself. Height fixed; width fills the card. */
|
||||
.ov-trend-spark {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
stroke: var(--status-ok, #22c55e);
|
||||
stroke-width: 1.5;
|
||||
fill: none;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
/* N/A overlay wrapper -- anchors the overlay text above the sparkline.
|
||||
Positioned so overlay sits centered on the sparkline area. */
|
||||
.ov-trend-spark-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Shown on FSSI-affected charts (SNR, FWD%, UIMD, LIMD) when FSSI is
|
||||
active. Per v0.4 B4: overlay reads "N/A -- FSSI active" and line
|
||||
drawing is suppressed (handled by extractors returning null).
|
||||
pointer-events: none so the overlay never intercepts clicks. */
|
||||
.ov-trend-na-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
pointer-events: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* When FSSI-active, tone down the card's current-value number. */
|
||||
.ov-trend-card-na .ov-trend-num-na {
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Min/max labels overlaid on the sparkline's right edge. Show the raw
|
||||
data range (not the minSpan-floored display range) so operators can
|
||||
always read the actual values — important when minSpan stretches a
|
||||
tiny real variation into a taller chart. pointer-events: none so the
|
||||
labels never intercept interaction with the svg. */
|
||||
.ov-trend-spark-labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ov-trend-label-max,
|
||||
.ov-trend-label-min {
|
||||
/* Units are not repeated on these labels — the card's main value
|
||||
already shows the unit, and repeating it here adds clutter. */
|
||||
padding: 1px 2px;
|
||||
background: var(--bg-main);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
CONTEXTUAL WARNING NOTES (added 2026-05-31)
|
||||
Shared classes for the amber "this feature is gated / mode-restricted"
|
||||
messages that were previously hand-written inline on each page.
|
||||
Two variants:
|
||||
.warning-banner — block callout (tinted box) above a section
|
||||
.field-note — small inline note appended after a field/label
|
||||
IMPORTANT: these classes intentionally do NOT set `display`. Visibility is
|
||||
toggled by page JS via `element.style.display = ''` (show) / 'none' (hide),
|
||||
which relies on the element's own inline `display:none` default. Adding
|
||||
`display` here would break that toggling (a cleared inline value would fall
|
||||
through to this rule). Keep `style="display:none"` on the element.
|
||||
========================================================================== */
|
||||
.warning-banner {
|
||||
padding: 8px 14px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--status-warn);
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.field-note {
|
||||
font-size: 11px;
|
||||
color: var(--status-warn);
|
||||
margin-left: 8px;
|
||||
}
|
||||
/* Error state for .warning-banner — red instead of amber (e.g. expired cert).
|
||||
Toggle `is-error` in JS instead of setting inline colors. */
|
||||
.warning-banner.is-error {
|
||||
color: var(--status-err);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SECONDARY (CANCEL) BUTTON (added 2026-05-31)
|
||||
Neutral-grey companion to .btn-modern, for Cancel / dismiss actions (e.g. the
|
||||
account-settings modal). Use: class="btn-modern btn-secondary"
|
||||
It only overrides the colour of .btn-modern; size/shape/hover come from
|
||||
.btn-modern. Theme-aware via --bg-card-hover.
|
||||
========================================================================== */
|
||||
input[type="button"].btn-secondary,
|
||||
input[type="submit"].btn-secondary {
|
||||
background-color: var(--bg-card-hover);
|
||||
}
|
||||
input[type="button"].btn-secondary:hover,
|
||||
input[type="submit"].btn-secondary:hover {
|
||||
background-color: var(--border-subtle);
|
||||
border-color: var(--border-subtle);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
PROGRESS BARS (added 2026-05-31)
|
||||
Restart / reset / firmware-update progress. The bar fill is styled via the
|
||||
browser pseudo-elements (the plain `color` property does NOT colour a
|
||||
<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
@@ -14,8 +14,32 @@
|
||||
// Power decimal lookup table
|
||||
var POWER_DECIMALS = ["00","05","10","15","20","25","30","35","40","45","50","55","60","65","70","75","80","85","90","95"];
|
||||
|
||||
// Input source labels
|
||||
var INPUT_SOURCES = ["ASI1","ASI2","EAS","Tuner","TSoIP","None"];
|
||||
// Input source labels — C1 (2026-08-05): smod's OWN status map. Index 5 = SMPTE per
|
||||
// smod firmware (matches the status page's proven parse). Copied by LABEL from the
|
||||
// family, values re-read from this product (register W9 — never copy values).
|
||||
// SMPTE slot PROVISIONAL pending the board-path hardware answer (team question).
|
||||
// "None" is NOT a source: it renders only for absent/out-of-range data via
|
||||
// getInputSource(). Frozen so no page can patch it into a parallel list again.
|
||||
var INPUT_SOURCES = Object.freeze(["ASI1","ASI2","EAS","Tuner","TSoIP","SMPTE"]);
|
||||
|
||||
/**
|
||||
* Map a firmware input-source index to its display name.
|
||||
* The ONLY translator from form[1] to a label — status_info.html calls this too
|
||||
* (C1 killed its private ternary). Out-of-range/absent -> "None" (genuine no-input).
|
||||
*/
|
||||
function getInputSource(index) {
|
||||
var i = Number(index);
|
||||
return (isFinite(i) && i >= 0 && i < INPUT_SOURCES.length) ? INPUT_SOURCES[i] : "None";
|
||||
}
|
||||
|
||||
/**
|
||||
* R-2 (2026-08-05): NaN guard for numeric card population (in-tree pattern from
|
||||
* sys_spectrum.html). Finite -> formatted via fn; else "--" (readback convention).
|
||||
*/
|
||||
function fmtNum(v, fn) {
|
||||
var n = Number(v);
|
||||
return isFinite(n) ? fn(n) : "--";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate frequency from channel number
|
||||
@@ -42,6 +66,7 @@
|
||||
* @returns {number|string} Temperature in Fahrenheit
|
||||
*/
|
||||
function convertTemperature(rawValue, unit) {
|
||||
if (!isFinite(Number(rawValue))) return "--"; // R-2: absent data -> "--", never NaN
|
||||
if (unit === 0) {
|
||||
return rawValue; // Already Fahrenheit
|
||||
} else {
|
||||
@@ -74,21 +99,32 @@
|
||||
* @returns {string} Status class
|
||||
*/
|
||||
function getSnrStatus(snr) {
|
||||
if (snr === "N/A") return 'off';
|
||||
var val = parseFloat(snr); // already carries the +10 dB display offset
|
||||
if (val >= 37) return 'ok';
|
||||
if (val >= 30) return 'warn';
|
||||
// "N/A" and "N/A-FSSI" both mean no meaningful reading -> grey, never red.
|
||||
// (FSSI parity round; the isNaN belt matches the 1.0 core's fall-through.)
|
||||
if (typeof snr === "string" && snr.indexOf("N/A") === 0) return 'off';
|
||||
var val = parseFloat(snr); // true measured SNR/MER in dB
|
||||
if (isNaN(val)) return 'off';
|
||||
if (val >= 27) return 'ok';
|
||||
if (val >= 20) return 'warn';
|
||||
return 'err';
|
||||
}
|
||||
|
||||
function updateSpectrumFssi(aFssi, bFssi, abMode) {
|
||||
var fssi = document.getElementById('rd_fssi');
|
||||
if (!fssi) return;
|
||||
fssi.innerText = (abMode === 1) ? (aFssi + ' / ' + bFssi) : aFssi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IMD status (UIMD/LIMD)
|
||||
* @param {string} imd - IMD value or "N/A"
|
||||
* @returns {string} Status class
|
||||
*/
|
||||
function getImdStatus(imd) {
|
||||
if (imd === "N/A") return 'off';
|
||||
return parseFloat(imd) >= 47 ? 'ok' : 'err';
|
||||
if (typeof imd === "string" && imd.indexOf("N/A") === 0) return 'off';
|
||||
var v = parseFloat(imd);
|
||||
if (isNaN(v)) return 'off';
|
||||
return v >= 47 ? 'ok' : 'err';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +162,10 @@
|
||||
(data.ctrl.toUpperCase() === "REMOTE") ? 'neutral' : 'warn');
|
||||
setCard('sum_rf', 'dot_rf', (data.rfStat === 1) ? "On" : "Off",
|
||||
(data.rfStat === 1) ? 'ok' : 'off');
|
||||
setCard('sum_outpow', 'dot_outpow', data.finalPower, 'ok');
|
||||
// REG-5 (2026-08-05, 1.0 C.4 parity): OUTPUT POWER must not stay green with a
|
||||
// stale figure while RF is off/muted — grey it whenever RF is not On.
|
||||
setCard('sum_outpow', 'dot_outpow', data.finalPower,
|
||||
(data.rfStat === 1) ? 'ok' : 'off');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,19 +225,25 @@
|
||||
var chan = Number(form[7]);
|
||||
var freqVal = calcFrequency(chan);
|
||||
|
||||
// SNR: half-dB from the app plus the fixed +10 dB display offset.
|
||||
var snr = (ab_modenum === 1) ? "N/A" : (Number(form[11]) / 2 + 10).toFixed(1);
|
||||
// SNR: half-dB value from the app, displayed as the true measured SNR/MER.
|
||||
var snr = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[11], function(n){ return (n / 2).toFixed(1); });
|
||||
|
||||
// LIMD / UIMD: half-dB from the app plus the fixed +11 dB display offset.
|
||||
// Rounding the sum and then appending the half misreports odd half-dB
|
||||
// values by 1 dB, so format the sum directly.
|
||||
var limd = (ab_modenum === 1) ? "N/A" : (Number(form[12]) / 2 + 11).toFixed(1);
|
||||
var uimd = (ab_modenum === 1) ? "N/A" : (Number(form[13]) / 2 + 11).toFixed(1);
|
||||
// LIMD / UIMD are half-dB values from the app. Apply the fixed +11 dB
|
||||
// display offset used by Status and the LCD.
|
||||
var limd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[12], function(n){ return (n / 2 + 11).toFixed(1); });
|
||||
var uimd = (ab_modenum === 1) ? "N/A-FSSI" : fmtNum(form[13], function(n){ return (n / 2 + 11).toFixed(1); });
|
||||
|
||||
// Input source
|
||||
var inputIdx = Number(form[1]);
|
||||
if (inputIdx < 0 || inputIdx > 5) inputIdx = 5;
|
||||
var inputSrc = INPUT_SOURCES[inputIdx];
|
||||
// The status interface already carries the shared A/B FSSI values.
|
||||
// Spectrum consumes the same values instead of issuing another request.
|
||||
updateSpectrumFssi(Number(form[16]), Number(form[17]), ab_modenum);
|
||||
window.fssiRefreshActive = (ab_modenum === 1);
|
||||
if (typeof window.setSpectrumRefreshActive === 'function') {
|
||||
window.setSpectrumRefreshActive(window.fssiRefreshActive);
|
||||
}
|
||||
|
||||
// Input source — C1: via the single accessor; the old >5 clamp is gone
|
||||
// (it is what rendered firmware idx 5 = SMPTE as "None").
|
||||
var inputSrc = getInputSource(form[1]);
|
||||
|
||||
// Input status
|
||||
var inputStatus = (Number(form[2]) === 0) ? "OK" : "Err";
|
||||
@@ -222,7 +267,8 @@
|
||||
var p_sign = (Number(rf_form[1]) === 0) ? "+" : "-";
|
||||
var p_int = Number(rf_form[2]);
|
||||
var p_dec = POWER_DECIMALS[Number(rf_form[3])] || "00";
|
||||
var finalPower = p_sign + p_int + "." + p_dec;
|
||||
// R-2: absent power data -> "--", never "+NaN.00"
|
||||
var finalPower = isFinite(p_int) ? (p_sign + p_int + "." + p_dec) : "--";
|
||||
|
||||
// Update all cards using the shared function
|
||||
window.updateDashboardCards({
|
||||
@@ -254,7 +300,8 @@
|
||||
getSnrStatus: getSnrStatus,
|
||||
getImdStatus: getImdStatus,
|
||||
POWER_DECIMALS: POWER_DECIMALS,
|
||||
INPUT_SOURCES: INPUT_SOURCES
|
||||
INPUT_SOURCES: INPUT_SOURCES,
|
||||
getInputSource: getInputSource
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -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 */ }
|
||||
};
|
||||
})();
|
||||
+25
-15
@@ -4,7 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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/jquery.js"></script>
|
||||
<script src="js/dashboard.js"></script>
|
||||
@@ -299,12 +300,13 @@
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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="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_psip.html" title="TSID&PSIP">TSID&PSIP</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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -324,8 +326,15 @@
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -345,7 +354,8 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -371,16 +381,6 @@
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
|
||||
@@ -391,6 +391,16 @@
|
||||
<span class="summary-unit">dBc</span>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
|
||||
+16
-9
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
<style>
|
||||
#content .box table td:first-child {
|
||||
@@ -40,13 +41,13 @@ function loginsubmit() {
|
||||
}
|
||||
|
||||
function refresh_show(){
|
||||
var queryData = "web_title=&"
|
||||
$.get("atsc_status_cfg.cgi?"+queryData, function(data, status) {
|
||||
var table = data.split("|");
|
||||
var title = table[0].split("#");
|
||||
document.getElementById('webtitle').textContent = title[1];
|
||||
document.getElementById('webtitle_header').textContent = title[1];
|
||||
});
|
||||
/* atsc_status_cfg.cgi requires a session, but the login page has none yet. */
|
||||
$.get("login.cgi?action=get_title", function(data) {
|
||||
var titleText = String(data || "").replace(/^#/, "").trim() || "Digital Exciter";
|
||||
document.title = titleText;
|
||||
document.getElementById('webtitle').textContent = titleText;
|
||||
document.getElementById('webtitle_header').textContent = titleText;
|
||||
}, "text");
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -73,7 +74,13 @@ function refresh_show(){
|
||||
</svg>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
<main class="app-main">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+145
-61
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title id="webtitle"></title>
|
||||
<script src="js/theme.js"></script>
|
||||
<link href="css/styles.css" rel="stylesheet" media="screen">
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/jquery.js"></script>
|
||||
@@ -41,7 +42,19 @@ function get_remote_local_ctrl() {
|
||||
var table = data.split("|");
|
||||
// Logic for remote/local control if needed
|
||||
// g_remot_local_ctrl = Number(table[0].split("#")[5]);
|
||||
document.title = table[0].split("#")[11];
|
||||
/* P-4 ROOT CAUSE + the better fix (2026-08-06).
|
||||
What was here: document.title = table[0].split("#")[11]; — copied from the
|
||||
1.0 BY VALUE, index and all (the same copy-by-value class as C1's
|
||||
INPUT_SOURCES). The 1.0's sys_cfg carries five NTP fields after ctl so its
|
||||
title sits at index 11; smod's sys_cfg has no NTP fields there, so index 11
|
||||
is ab_mode — the FSSI Refresh switch. The tab was displaying the FSSI switch
|
||||
state: "0" or "1", flipping as FSSI was toggled (owner, 2026-08-06).
|
||||
Why the line is DELETED rather than re-indexed: this page already has a
|
||||
correct, authoritative title source — the dedicated web_title section polled
|
||||
in SetFormDefaults below (table[7]). Two writers for one value is the defect,
|
||||
not the index; re-indexing would have left a second writer racing the first
|
||||
(and is exactly what C1 removed on the input-source axis). One source of
|
||||
truth: the web_title section. This function now only does what its name says. */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +64,11 @@ function get_remote_local_ctrl() {
|
||||
}
|
||||
|
||||
function SetFormDefaults() {
|
||||
// R-2 (2026-08-05): NaN guards — in-tree pattern from sys_spectrum.html. Readbacks
|
||||
// show "--" for absent data; finite values format as before.
|
||||
function fmtScaled(v, scale, dp){ var n = Number(v); return isFinite(n) ? (n*scale).toFixed(dp) : "--"; }
|
||||
function fmtRaw(v){ var n = Number(v); return isFinite(n) ? n : "--"; }
|
||||
|
||||
var queryData = "State_SysState=&tuner_status=&pac_status=&gps_status=&State_SysVer=&mac_sn_status=&sys_rf=&web_title=&"
|
||||
$.get("atsc_status_cfg.cgi?"+queryData, function(data, status) {
|
||||
var temp_unit=1;
|
||||
@@ -63,10 +81,23 @@ function SetFormDefaults() {
|
||||
var ab_modenum = Number(form[25]);
|
||||
temp_unit = Number(form[26]);
|
||||
|
||||
// Input Source Map
|
||||
var i = Number(form[1]);
|
||||
var s_str = (i==0)?"ASI1":(i==1)?"ASI2":(i==2)?"EAS":(i==3)?"Tuner":(i==4)?"TSoIP":(i==5)?"SMPTE":"None";
|
||||
cf.asi_input.value = s_str;
|
||||
// Input Source — C1 (2026-08-05): the page-private ternary is DELETED; this and the
|
||||
// dashboard cards read the ONE map via DashboardHelpers.getInputSource (the two lists
|
||||
// disagreed at index 5 — SMPTE vs None — which is your "None for a selected input").
|
||||
cf.asi_input.value = window.DashboardHelpers.getInputSource(form[1]);
|
||||
|
||||
/* REG-2 / ST.F11 port (2026-08-05, from the 1.0): auto-expand Tuner Status on
|
||||
ENTERING tuner/translator mode — smod IS a translator, so this is its primary
|
||||
mode. Same "changed" semantics as the 1.0's adaptation of 3.0's publishMode:
|
||||
expand only on the transition INTO Tuner input (form[1]===3, same index here,
|
||||
proven by this page's own map), tracked by _stTunerMode — NEVER on every poll,
|
||||
so an operator's manual collapse is not fought. Box-headers exist by now
|
||||
(initCollapsibleTables runs at DOMContentLoaded, before this callback). */
|
||||
(function(){
|
||||
var inTuner = (Number(form[1]) === 3);
|
||||
if (inTuner && window._stTunerMode !== true) { autoExpandTunerStatus(); }
|
||||
window._stTunerMode = inTuner;
|
||||
})();
|
||||
|
||||
// Input Status
|
||||
cf.asi_status.value = (Number(form[2])==0) ? "OK" : "Err";
|
||||
@@ -76,12 +107,15 @@ function SetFormDefaults() {
|
||||
|
||||
cf.ts_rate.value = form[4]; // TS Rate
|
||||
|
||||
// Temperature
|
||||
if(temp_unit==0){
|
||||
// Temperature — R-2: absent data -> "--", never NaN cascades
|
||||
var t_raw = Number(form[5]);
|
||||
if(!isFinite(t_raw)){
|
||||
cf.temp1.value = "--"; cf.temp1_c.value = "--";
|
||||
} else if(temp_unit==0){
|
||||
cf.temp1.value = form[5];
|
||||
cf.temp1_c.value = ((cf.temp1.value - 32)/1.8).toFixed(2);
|
||||
cf.temp1_c.value = ((t_raw - 32)/1.8).toFixed(2);
|
||||
} else {
|
||||
cf.temp1.value = (form[5]*1.8+32).toFixed(2);
|
||||
cf.temp1.value = (t_raw*1.8+32).toFixed(2);
|
||||
cf.temp1_c.value = form[5];
|
||||
}
|
||||
|
||||
@@ -102,27 +136,27 @@ function SetFormDefaults() {
|
||||
|
||||
// Offset
|
||||
i = Number(form[8]);
|
||||
s_str = (i==0) ? "+" : "-";
|
||||
var s_str = (i==0) ? "+" : "-"; // scratch var — its declaration moved here when C1 deleted the input-source ternary that used to declare it
|
||||
cf.hz1.value = s_str + form[9];
|
||||
|
||||
// FWD Power & SNR
|
||||
var rawFwd = Number(form[10]);
|
||||
cf.fwd.value = (ab_modenum==1) ? "N/A" : (rawFwd + " %");
|
||||
cf.fwd.value = (ab_modenum==1) ? "N/A-FSSI" : (rawFwd + " %");
|
||||
|
||||
// SNR. Half-dB from the app, shown as-is (PR12 removed the old fixed
|
||||
// +10 dB display offset; this is the true measured MER).
|
||||
var tmpdata = Number(form[11]);
|
||||
var snrStr = (tmpdata/2).toFixed(1);
|
||||
cf.tsnr.value = (ab_modenum==1) ? "N/A" : snrStr;
|
||||
cf.tsnr.value = (ab_modenum==1) ? "N/A-FSSI" : snrStr;
|
||||
|
||||
// IMD. Half-dB from the app, plus the fixed +11 dB display offset.
|
||||
// (Do not Math.round() the sum and then paste on the half: for an odd
|
||||
// half-dB value that rounds x.5 up and reports 1 dB too much.)
|
||||
tmpdata = Number(form[12]);
|
||||
cf.l_imd.value = (ab_modenum==1) ? "N/A" : (tmpdata/2 + 11).toFixed(1);
|
||||
cf.l_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1);
|
||||
|
||||
tmpdata = Number(form[13]);
|
||||
cf.u_imd.value = (ab_modenum==1) ? "N/A" : (tmpdata/2 + 11).toFixed(1);
|
||||
cf.u_imd.value = (ab_modenum==1) ? "N/A-FSSI" : (tmpdata/2 + 11).toFixed(1);
|
||||
|
||||
// Control & Amp
|
||||
cf.f1ctrl.value = (Number(form[14])==0) ? "Remote" : "Local";
|
||||
@@ -143,6 +177,13 @@ function SetFormDefaults() {
|
||||
cf.b_fssi.value = (ab_modenum==1) ? (Number(form[17]) + " %") : "N/A";
|
||||
|
||||
// AGC State
|
||||
/* FSSI active alternates the RFinA feedback path AGC realizes on,
|
||||
so AGC state (and its error codes) are not meaningful — show the
|
||||
reason instead of a misleading adjustment value. Takes priority.
|
||||
(Ported from the 1.0, status_info.html:239-243 — FSSI parity round.) */
|
||||
if(ab_modenum==1){
|
||||
s_str = "N/A-FSSI";
|
||||
} else {
|
||||
agc_err_code = Number(form[27]);
|
||||
var agc_adj = Number(form[28]);
|
||||
if(agc_err_code==1) s_str="Down Limit!";
|
||||
@@ -154,6 +195,7 @@ function SetFormDefaults() {
|
||||
s_str="OK";
|
||||
}
|
||||
}
|
||||
}
|
||||
cf.agc_state.value = s_str;
|
||||
|
||||
// --- 2. PARSE SYS_RF FOR DASHBOARD ---
|
||||
@@ -191,37 +233,40 @@ function SetFormDefaults() {
|
||||
// Form 1: Tuner
|
||||
var form1 = table[1].split("#");
|
||||
var cf1 = document.getElementById("tuner");
|
||||
cf1.rx_freq.value = Number(form1[1]);
|
||||
cf1.rssi.value = Number(form1[2]);
|
||||
cf1.rsnr.value = Number(form1[3]);
|
||||
cf1.rx_freq.value = fmtRaw(form1[1]);
|
||||
cf1.rssi.value = fmtRaw(form1[2]);
|
||||
cf1.rsnr.value = fmtRaw(form1[3]);
|
||||
cf1.lock.selectedIndex = Number(form1[4]);
|
||||
|
||||
// Form 2: PAC Status
|
||||
var form2 = table[2].split("#");
|
||||
var cf2 = document.getElementById("pac_status");
|
||||
cf2.v9v.value = (Number(form2[1])*0.1).toFixed(2);
|
||||
cf2.v12v.value = (Number(form2[2])*0.1).toFixed(2);
|
||||
cf2.v50v.value = (Number(form2[3])*0.1).toFixed(2);
|
||||
cf2.pac_fwd.value = (Number(form2[4])*0.1).toFixed(2);
|
||||
cf2.ref.value = (Number(form2[5])*0.1).toFixed(2);
|
||||
cf2.vswr.value = (Number(form2[6])*0.1).toFixed(2);
|
||||
cf2.v9v.value = fmtScaled(form2[1], 0.1, 2);
|
||||
cf2.v12v.value = fmtScaled(form2[2], 0.1, 2);
|
||||
cf2.v50v.value = fmtScaled(form2[3], 0.1, 2);
|
||||
cf2.pac_fwd.value = fmtScaled(form2[4], 0.1, 2);
|
||||
cf2.ref.value = fmtScaled(form2[5], 0.1, 2);
|
||||
cf2.vswr.value = fmtScaled(form2[6], 0.1, 2);
|
||||
|
||||
// PAC Temp
|
||||
if(temp_unit==0){
|
||||
cf2.temp.value = (Number(form2[7])*0.1).toFixed(2);
|
||||
// PAC Temp — R-2 guarded
|
||||
var pac_t = Number(form2[7]);
|
||||
if(!isFinite(pac_t)){
|
||||
cf2.temp.value = "--"; cf2.temp_c.value = "--";
|
||||
} else if(temp_unit==0){
|
||||
cf2.temp.value = (pac_t*0.1).toFixed(2);
|
||||
cf2.temp_c.value = (cf2.temp.value < 32) ? 0 : ((cf2.temp.value - 32)/1.8).toFixed(2);
|
||||
} else {
|
||||
cf2.temp.value = (Number(form2[7])*0.1*1.8+32).toFixed(2);
|
||||
cf2.temp_c.value = (Number(form2[7])*0.1).toFixed(2);
|
||||
}
|
||||
cf2.temp.value = (pac_t*0.1*1.8+32).toFixed(2);
|
||||
cf2.temp_c.value = (pac_t*0.1).toFixed(2);
|
||||
}
|
||||
|
||||
cf2.pain.value = Number(form2[8]); // This is PA_IN
|
||||
|
||||
cf2.c50v1.value = (Number(form2[9])*0.1).toFixed(2);
|
||||
cf2.c50v2.value = (Number(form2[10])*0.1).toFixed(2);
|
||||
cf2.c50v3.value = (Number(form2[11])*0.1).toFixed(2);
|
||||
cf2.c50v4.value = (Number(form2[12])*0.1).toFixed(2);
|
||||
cf2.gv.value = (Number(form2[13])*0.1).toFixed(2);
|
||||
cf2.c50v1.value = fmtScaled(form2[9], 0.1, 2);
|
||||
cf2.c50v2.value = fmtScaled(form2[10], 0.1, 2);
|
||||
cf2.c50v3.value = fmtScaled(form2[11], 0.1, 2);
|
||||
cf2.c50v4.value = fmtScaled(form2[12], 0.1, 2);
|
||||
cf2.gv.value = fmtScaled(form2[13], 0.1, 2);
|
||||
cf2.lvl.selectedIndex = Number(form2[14]);
|
||||
|
||||
// Form 3: GPS
|
||||
@@ -252,8 +297,11 @@ function SetFormDefaults() {
|
||||
if (table[7]) {
|
||||
var title = table[7].split("#");
|
||||
var titleStr = (title[1] != null) ? title[1] : "";
|
||||
document.getElementById('webtitle').textContent = titleStr;
|
||||
document.getElementById('webtitle_header').textContent = titleStr;
|
||||
/* P-4 guard: never paint a short/absent read over a good title (R-2 class). */
|
||||
if (titleStr !== "") {
|
||||
document.getElementById('webtitle').textContent = titleStr;
|
||||
document.getElementById('webtitle_header').textContent = titleStr;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Status Colors and UI helpers
|
||||
@@ -330,14 +378,15 @@ function highlightProblemTableRows() {
|
||||
}
|
||||
// SNR, LIMD, UIMD: same error thresholds as summary cards / updateStatusColors
|
||||
var id = inp.id || '';
|
||||
if (id === 'tsnr' && v !== '' && v !== 'N/A') {
|
||||
/* v.indexOf('N/A') !== 0 excludes both "N/A" and "N/A-FSSI" (FSSI-active) — 1.0 :413 pattern */
|
||||
if (id === 'tsnr' && v !== '' && v.indexOf('N/A') !== 0) {
|
||||
var snrNum = parseFloat(v); // true measured dB (PR12: no display offset)
|
||||
if (snrNum < 15) { tr.classList.add('row-has-error'); break; }
|
||||
}
|
||||
if (id === 'l_imd' && v !== '' && v !== 'N/A') {
|
||||
if (id === 'l_imd' && v !== '' && v.indexOf('N/A') !== 0) {
|
||||
if (parseFloat(v) < 47) { tr.classList.add('row-has-error'); break; }
|
||||
}
|
||||
if (id === 'u_imd' && v !== '' && v !== 'N/A') {
|
||||
if (id === 'u_imd' && v !== '' && v.indexOf('N/A') !== 0) {
|
||||
if (parseFloat(v) < 47) { tr.classList.add('row-has-error'); break; }
|
||||
}
|
||||
// License fields: INACTIVE is an error
|
||||
@@ -381,7 +430,7 @@ function updateStatusColors() {
|
||||
|
||||
// Main Status details: LIMD, UIMD, SNR – same rules as summary cards (green/red/warn by range)
|
||||
var tsnrEl = document.getElementById("tsnr");
|
||||
if(tsnrEl && tsnrEl.value !== "" && tsnrEl.value !== "N/A") {
|
||||
if(tsnrEl && tsnrEl.value !== "" && tsnrEl.value.indexOf("N/A") !== 0) {
|
||||
var snrVal = parseFloat(tsnrEl.value); // true measured dB (PR12: no display offset)
|
||||
if(snrVal >= 27) tsnrEl.style.color = color_ok;
|
||||
else if(snrVal >= 20) tsnrEl.style.color = color_warn;
|
||||
@@ -389,12 +438,12 @@ function updateStatusColors() {
|
||||
} else if(tsnrEl) tsnrEl.style.color = "";
|
||||
|
||||
var limdEl = document.getElementById("l_imd");
|
||||
if(limdEl && limdEl.value !== "" && limdEl.value !== "N/A") {
|
||||
if(limdEl && limdEl.value !== "" && limdEl.value.indexOf("N/A") !== 0) {
|
||||
limdEl.style.color = parseFloat(limdEl.value) >= 47 ? color_ok : color_err;
|
||||
} else if(limdEl) limdEl.style.color = "";
|
||||
|
||||
var uimdEl = document.getElementById("u_imd");
|
||||
if(uimdEl && uimdEl.value !== "" && uimdEl.value !== "N/A") {
|
||||
if(uimdEl && uimdEl.value !== "" && uimdEl.value.indexOf("N/A") !== 0) {
|
||||
uimdEl.style.color = parseFloat(uimdEl.value) >= 47 ? color_ok : color_err;
|
||||
} else if(uimdEl) uimdEl.style.color = "";
|
||||
|
||||
@@ -599,7 +648,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Collapsible Tables - MAIN STATUS, PAC STATUS, GPS STATUS
|
||||
/* ST.F11 support (2026-08-05): expand the Tuner Status box if collapsed. Called only on
|
||||
the transition into tuner/translator mode by the data callback — see the port note there. */
|
||||
function autoExpandTunerStatus() {
|
||||
var headers = document.querySelectorAll('.box-header');
|
||||
for (var i = 0; i < headers.length; i++) {
|
||||
var titleSpan = headers[i].querySelector('span');
|
||||
if (titleSpan && titleSpan.innerText.indexOf('Tuner Status') !== -1) {
|
||||
if (headers[i].classList.contains('collapsed')) {
|
||||
headers[i].classList.remove('collapsed');
|
||||
var content = headers[i].nextElementSibling;
|
||||
if (content && content.className.indexOf('box-content') !== -1) {
|
||||
content.style.display = 'block';
|
||||
}
|
||||
var badge = headers[i].querySelector('.toggle-text');
|
||||
if (badge) badge.innerText = 'Hide Details';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsible Tables - MAIN STATUS, TUNER STATUS, PAC STATUS, GPS STATUS
|
||||
function initCollapsibleTables() {
|
||||
var headers = document.querySelectorAll('.app-main h2');
|
||||
for (var i = 0; i < headers.length; i++) {
|
||||
@@ -621,20 +691,26 @@ function initCollapsibleTables() {
|
||||
}
|
||||
|
||||
if (box) {
|
||||
/* REG-1 (2026-08-05, 1.0 C.2 port — "3.0 parity"): the primary status block
|
||||
is OPEN on load so the operator sees it without a click; every other
|
||||
section starts collapsed. Tuner Status is additionally auto-expanded on
|
||||
entering translator mode by the data callback (ST.F11) — it cannot be
|
||||
decided here because the mode is not yet read at init. */
|
||||
var startOpen = (headerText === "Main Status");
|
||||
var boxHeader = document.createElement('div');
|
||||
boxHeader.className = "box-header collapsed";
|
||||
boxHeader.className = startOpen ? "box-header" : "box-header collapsed";
|
||||
var titleSpan = document.createElement('span');
|
||||
titleSpan.innerText = headerText + " Details";
|
||||
var toggleSpan = document.createElement('span');
|
||||
toggleSpan.className = "toggle-text";
|
||||
toggleSpan.innerText = "View Details";
|
||||
toggleSpan.innerText = startOpen ? "Hide Details" : "View Details";
|
||||
|
||||
boxHeader.appendChild(titleSpan);
|
||||
boxHeader.appendChild(toggleSpan);
|
||||
|
||||
var contentDiv = document.createElement('div');
|
||||
contentDiv.className = "box-content";
|
||||
contentDiv.style.display = "none";
|
||||
contentDiv.style.display = startOpen ? "block" : "none";
|
||||
|
||||
while (box.firstChild) {
|
||||
contentDiv.appendChild(box.firstChild);
|
||||
@@ -683,17 +759,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body onload="refresh_show()">
|
||||
<body onload="refresh_show()">
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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><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_psip.html" title="TSID&PSIP">TSID&PSIP</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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -713,8 +789,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -734,7 +817,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -760,16 +844,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<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>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
|
||||
+120
-18
@@ -4,7 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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/jquery.js"></script>
|
||||
<script src="js/dashboard.js"></script>
|
||||
@@ -57,10 +58,99 @@
|
||||
|
||||
var table = data.split("|");
|
||||
g_remot_local_ctrl = Number(table[0].split("#")[5]);
|
||||
document.title = table[0].split("#")[11];
|
||||
/* P-4b (2026-08-06): the title write here — document.title =
|
||||
table[0].split("#")[11] — is DELETED. On smod index 11 is ab_mode
|
||||
(the FSSI Refresh switch), not the web title. This page already sets
|
||||
the title from its web_title section below. Round 3 fixed only
|
||||
status_info; this is the rest of that class. */
|
||||
});
|
||||
}
|
||||
|
||||
/* REG-4 (2026-08-05) — CTRL gate, ported from the 1.0 (its C.5, 3.0 SLT.7 parity; the
|
||||
owner-found DESTRUCTIVE class: saving TSID/program rows while CTRL != Enable writes
|
||||
non-effective values the operator believes took). When CTRL (ts_edit) != Enable(1),
|
||||
the TSID save and every program-row save are disabled. The CTRL row's own Set
|
||||
(#ts_ctrl_set) stays live so CTRL can be changed (CTRL+TSID share one post here, as
|
||||
on the 1.0 — changing CTRL via that button is the intended path). No-op in Local
|
||||
mode, where SetFormDefaults has already disabled everything. Safe on load and on
|
||||
every ts_edit change. */
|
||||
/* P-6 (2026-08-06, CORRECTED after owner review). What the licence gate must do —
|
||||
matched to what the 1.0 (sys_psip.html:157-168) and the 3.0 actually do, and to the
|
||||
owner's ruling "no licence should STILL ALLOW Clear":
|
||||
|
||||
no PSIP licence -> remove ONLY the "Enable" choice from ts_edit.
|
||||
Disable(0) and Clear(2) stay fully usable; every Set stays live; NO grey-out,
|
||||
NO banner. The operator can still disable or clear PSIP without a licence; they
|
||||
simply cannot ENABLE it.
|
||||
|
||||
The prior version of this function was the reverse of that — it forced the value off
|
||||
Enable, disabled both Sets and the whole program table, and showed a banner. That
|
||||
locked out Clear, which is exactly backwards. Corrected here.
|
||||
|
||||
R4 (2026-08-11) — THE PREVIOUS COMMENT HERE WAS WRONG AND IS CORRECTED.
|
||||
It claimed the 1.0's $('option').hide() "browsers do not honour reliably", promoted
|
||||
from an ambiguous field report. Retrieval settled it: the 1.0's TSoIP page has NO
|
||||
licence handling at all (zero references in sys_tsoip.html), so the option the owner
|
||||
saw selectable cannot have been a 1.0 TSoIP gate; and smod had no gate whatsoever
|
||||
before round 3, which explains "selectable on both" on its own. The owner then
|
||||
confirmed the 1.0's Enable option DISAPPEARS when unlicensed — i.e. hiding works.
|
||||
Owner ruling 2026-08-11: the option must be HIDDEN, not shown-but-unselectable.
|
||||
|
||||
So the gate now hides AND disables: hidden removes it from the list (1.0 parity, and
|
||||
what the owner sees), disabled is the belt for any engine that renders a hidden
|
||||
option anyway. Disable(0) and Clear(2) and every Set remain untouched. */
|
||||
var g_psip_licensed = null; // null = not yet known; never gate on an unknown
|
||||
function applyLicenceGate(){
|
||||
var edit = document.getElementById("ts_edit");
|
||||
if (!edit) return;
|
||||
var enableOpt = edit.querySelector('option[value="1"]');
|
||||
if (!enableOpt) return;
|
||||
if (g_psip_licensed === false) {
|
||||
enableOpt.hidden = true; // out of the list entirely (owner ruling)
|
||||
enableOpt.style.display = "none"; // same effect for engines ignoring [hidden]
|
||||
enableOpt.disabled = true; // belt: unselectable even if rendered
|
||||
if (Number(edit.value) === 1) { edit.value = "0"; } // if it was on Enable, fall back to Disable
|
||||
} else {
|
||||
enableOpt.hidden = false; // licensed or unknown -> Enable available
|
||||
enableOpt.style.display = "";
|
||||
enableOpt.disabled = false;
|
||||
}
|
||||
// Disable/Clear and every Set are deliberately left ALONE — Clear must work unlicensed.
|
||||
}
|
||||
function get_licence(){
|
||||
$.get("atsc_status_cfg.cgi?sn_license=&", function(data, status){
|
||||
if (status !== "success" || !data) return; // unknown -> no gate
|
||||
var licence_para = data.split("|")[0].split("#");
|
||||
var raw = licence_para[2];
|
||||
var key_psip = Number(raw);
|
||||
/* Fail-open on a garbled read (round-1 bench caught the fail-closed version): only a
|
||||
clean 0/1 is a licence answer; anything else stays unknown and ungated. */
|
||||
if (raw === undefined || raw === "" || isNaN(key_psip)) return;
|
||||
g_psip_licensed = (key_psip === 1);
|
||||
applyLicenceGate();
|
||||
});
|
||||
}
|
||||
|
||||
function applyCtrlGate() {
|
||||
if (g_remot_local_ctrl == 1) return; // Local: all already disabled
|
||||
var edit = document.getElementById("ts_edit");
|
||||
var enabled = edit && Number(edit.value) === 1; // 1 = Enable
|
||||
if (enabled) return; // Enable: normal Set state stands
|
||||
// Not Enable -> block dependent saves; leave #ts_ctrl_set live.
|
||||
var tsidBtn = document.getElementById("ts_tsid_set");
|
||||
if (tsidBtn) { tsidBtn.disabled = true; tsidBtn.value = "Set"; }
|
||||
$('#psip_table_form input[type="button"]').prop("disabled", true).val("Set");
|
||||
}
|
||||
// REG-4: re-gate the moment the operator changes CTRL in the form (registered once)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tsEditEl = document.getElementById('ts_edit');
|
||||
if (tsEditEl) tsEditEl.addEventListener('change', function () {
|
||||
applyCtrlGate();
|
||||
applyLicenceGate(); // P-6: licence outranks CTRL — re-assert on every change
|
||||
});
|
||||
get_licence(); // P-6: read the licence once, gate when it answers
|
||||
});
|
||||
|
||||
// Updated SetFormDefaults to populate the Single Form
|
||||
function SetFormDefaults() {
|
||||
// 1. DISABLE BUTTONS START (only in Remote mode)
|
||||
@@ -133,6 +223,9 @@ function SetFormDefaults() {
|
||||
}
|
||||
if (typeof setTsidFormSnapshotUpdate === 'function') setTsidFormSnapshotUpdate();
|
||||
if (typeof psipFormSnapshotUpdate === 'function') psipFormSnapshotUpdate();
|
||||
// REG-4: gate dependent saves whenever fresh CTRL state lands (after the re-enable above)
|
||||
applyCtrlGate();
|
||||
applyLicenceGate(); // P-6: licence gate outranks CTRL gate; re-assert after every repaint
|
||||
|
||||
document.getElementById('webtitle').textContent = title[1];
|
||||
document.getElementById('webtitle_header').textContent = title[1];
|
||||
@@ -474,12 +567,13 @@ function cmd_confirm() {
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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="sys_set.html" title="Settings">Settings</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><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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -499,8 +593,15 @@ function cmd_confirm() {
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -520,7 +621,8 @@ function cmd_confirm() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -546,16 +648,6 @@ function cmd_confirm() {
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<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>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
@@ -659,7 +761,7 @@ function cmd_confirm() {
|
||||
</select>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -668,7 +770,7 @@ function cmd_confirm() {
|
||||
<input type="text" maxlength="5" size="8" name="tsid" id="tsid" value="">
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
+88
-22
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title id="webtitle"></title>
|
||||
<script src="js/theme.js"></script>
|
||||
<link href="css/styles.css" rel="stylesheet" media="screen">
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/jquery.js"></script>
|
||||
@@ -66,7 +67,11 @@ function get_remote_local_ctrl() {
|
||||
|
||||
var table = data.split("|");
|
||||
g_remot_local_ctrl = Number(table[0].split("#")[5]);
|
||||
document.title = table[0].split("#")[11];
|
||||
/* P-4b (2026-08-06): the title write here — document.title =
|
||||
table[0].split("#")[11] — is DELETED. On smod index 11 is ab_mode
|
||||
(the FSSI Refresh switch), not the web title. This page already sets
|
||||
the title from its web_title section below. Round 3 fixed only
|
||||
status_info; this is the rest of that class. */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -466,12 +471,13 @@ if (document.readyState === 'loading') {
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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="sys_set.html" title="Settings">Settings</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_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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -491,8 +497,15 @@ if (document.readyState === 'loading') {
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -512,7 +525,8 @@ if (document.readyState === 'loading') {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -538,16 +552,6 @@ if (document.readyState === 'loading') {
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<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>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
@@ -696,6 +710,21 @@ if (document.readyState === 'loading') {
|
||||
</td>
|
||||
</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>
|
||||
<td>AGC</td>
|
||||
<td>
|
||||
@@ -882,7 +911,30 @@ if (document.readyState === 'loading') {
|
||||
</table>
|
||||
</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>
|
||||
<div class="box">
|
||||
<table>
|
||||
<tr>
|
||||
<td>10 MHz Output</td>
|
||||
@@ -897,21 +949,35 @@ if (document.readyState === 'loading') {
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="padding: 4px 10px; color: #64748b; font-size: 12px;">
|
||||
Enables the AB5 10 MHz reference clock output. Default Off; resets to Off on every power-up.
|
||||
<!-- R8: standing informational note -> plain amber text, same rule as the -25 dBm note above. -->
|
||||
<!-- 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><!-- /.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>
|
||||
</div>
|
||||
|
||||
<div id="accountModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>ACCOUNT SETTINGS</h3>
|
||||
<h3>Account Settings</h3>
|
||||
<span class="close" onclick="closeAccountModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
+316
-73
@@ -3,8 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title id="webtitle"></title>
|
||||
<link href="css/styles.css" rel="stylesheet" media="screen">
|
||||
<title id="webtitle">Digital Exciter</title>
|
||||
<script src="js/theme.js"></script>
|
||||
<link href="css/styles.css" rel="stylesheet" media="screen">
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/jquery.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 label { font-size:13px; color:inherit; }
|
||||
.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 .k { display:block; font-size:11px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; }
|
||||
.spec-readout .v { display:block; font-size:18px; font-weight:600; }
|
||||
#spec_canvas { width:100%; height:420px; display:block; background:#0b1220; border-radius:6px; }
|
||||
#eye_canvas { width:100%; height:380px; display:block; background:#0b1220; 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-pair { display:flex; gap:10px; flex:0 0 auto; }
|
||||
.spec-readout .k { display:block; font-size:10px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; }
|
||||
.spec-readout .v { display:block; font-size:16px; font-weight:600; }
|
||||
#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; }
|
||||
#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>
|
||||
<script>
|
||||
var lastTime = new Date().getTime();
|
||||
@@ -46,7 +60,11 @@
|
||||
$.get("atsc_status_cfg.cgi?sys_cfg=&", function(data, status){
|
||||
if (typeof data === 'object' && data !== null) return;
|
||||
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 SH_LO = 3.5, SH_HI = 4.8; // shoulder band the app takes the median over
|
||||
var activeView = 'spectrum';
|
||||
var fssiRefreshActive = false;
|
||||
|
||||
function specInit(){
|
||||
window.addEventListener('resize', function(){ if (specLast) drawSpectrum(specLast); else drawGrid(); });
|
||||
drawGrid();
|
||||
specRestart();
|
||||
eyeRestart();
|
||||
window.addEventListener('resize', function(){
|
||||
if (activeView === 'spectrum') {
|
||||
if (specLast) drawSpectrum(specLast); else drawGrid();
|
||||
} else if (eyeLastData) {
|
||||
drawEye(eyeLastData);
|
||||
}
|
||||
});
|
||||
selectSpectrumView('spectrum');
|
||||
}
|
||||
|
||||
/* PR12: the Refresh/Pause/Peak-hold toolbar and the status lines were
|
||||
* removed on request, so these helpers tolerate the elements being absent
|
||||
* (fixed 1 s refresh, never paused, no peak-hold). */
|
||||
function selectSpectrumView(view) {
|
||||
activeView = (view === 'eye') ? 'eye' : 'spectrum';
|
||||
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 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(){
|
||||
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
|
||||
if (activeView !== 'spectrum') return;
|
||||
if (fssiRefreshActive) { setState('FSSI Refresh ON'); return; }
|
||||
if (elChecked('spec_pause')) { setState('paused'); return; }
|
||||
specFetch();
|
||||
SPEC_TIMER = setInterval(specFetch, specRateMs());
|
||||
@@ -94,6 +161,26 @@
|
||||
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; }
|
||||
|
||||
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(){
|
||||
$.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false})
|
||||
.done(function(d){
|
||||
@@ -102,18 +189,59 @@
|
||||
setState(d && d.error === 'nodata' ? 'no window captured yet' : 'error');
|
||||
return;
|
||||
}
|
||||
if (fssiRefreshActive) return;
|
||||
specFails = 0;
|
||||
specLast = d;
|
||||
updateReadouts(d);
|
||||
drawSpectrum(d);
|
||||
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
|
||||
if (activeView === 'spectrum') {
|
||||
updateReadouts(d);
|
||||
drawSpectrum(d);
|
||||
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
|
||||
}
|
||||
})
|
||||
.fail(function(){ specFails++; setState('fetch failed (' + specFails + ')'); });
|
||||
}
|
||||
|
||||
function updateReadouts(d){
|
||||
/* PR12: the readout cards under the spectrum were removed; d.shoulder
|
||||
* and d.pilot are still drawn on the plot itself in drawSpectrum(). */
|
||||
/* R7 (2026-08-11) + R10 (2026-08-12): apply the documented 11 dB pilot offset
|
||||
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(){
|
||||
if (EYE_TIMER) clearInterval(EYE_TIMER);
|
||||
if (activeView !== 'eye') return;
|
||||
if (fssiRefreshActive) { setEyeState('FSSI Refresh ON'); return; }
|
||||
if (elChecked('spec_pause')) return;
|
||||
eyeFetch();
|
||||
EYE_TIMER = setInterval(eyeFetch, specRateMs());
|
||||
@@ -142,24 +272,47 @@
|
||||
return;
|
||||
}
|
||||
if (!d.sync){ setEyeState('no symbol lock'); return; }
|
||||
if (fssiRefreshActive) return;
|
||||
eyeFails = 0;
|
||||
eyeLastData = d;
|
||||
eyeReadouts(d);
|
||||
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
|
||||
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
|
||||
if (activeView === 'eye') {
|
||||
eyeReadouts(d);
|
||||
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
|
||||
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
|
||||
}
|
||||
})
|
||||
.fail(function(){ eyeFails++; setEyeState('fetch failed (' + eyeFails + ')'); });
|
||||
}
|
||||
|
||||
function eyeReadouts(d){
|
||||
/* PR12: only SNR/MER and SNR raw remain on request. */
|
||||
document.getElementById('rd_snr').innerText = d.snr_db.toFixed(2);
|
||||
document.getElementById('rd_snrraw').innerText = (d.snr_raw_db === undefined) ? '--' : d.snr_raw_db.toFixed(2);
|
||||
var eyeHeights = Array.isArray(d.eye_h) ? d.eye_h.map(readNumber).filter(function(value){ return value !== null; }) : [];
|
||||
var sigma = Array.isArray(d.sigma) ? d.sigma.map(readNumber).filter(function(value){ return value !== null; }) : [];
|
||||
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');
|
||||
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){
|
||||
var P = PLOT();
|
||||
var c = document.getElementById('eye_canvas');
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var W = c.clientWidth, H = c.clientHeight;
|
||||
@@ -169,7 +322,7 @@
|
||||
var g = c.getContext('2d');
|
||||
g.setTransform(dpr,0,0,dpr,0,0);
|
||||
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 bin = atob(d.eye.data), x, y;
|
||||
@@ -196,30 +349,74 @@
|
||||
g.drawImage(off, L, T, W - L - R, 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){
|
||||
var yy = yOfLvl(v);
|
||||
g.beginPath(); g.moveTo(L,yy); g.lineTo(W-R,yy); g.stroke();
|
||||
});
|
||||
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.setLineDash([]);
|
||||
|
||||
g.fillStyle = '#94a3b8'; g.font = '11px sans-serif';
|
||||
g.fillStyle = P.axisLbl; g.font = '11px sans-serif';
|
||||
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)); });
|
||||
g.textAlign = 'center'; g.textBaseline = 'top';
|
||||
[-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.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.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();
|
||||
}
|
||||
|
||||
/* ---- 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(){
|
||||
var c = document.getElementById('spec_canvas');
|
||||
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 drawGrid(fmin, fmax){
|
||||
var P = PLOT();
|
||||
if (fmin === undefined){ fmin = -5; fmax = 5; }
|
||||
var m = specGeom(), g = m.g, f, db;
|
||||
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){
|
||||
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);
|
||||
@@ -251,18 +449,18 @@
|
||||
g.textAlign = 'center'; g.textBaseline = 'top';
|
||||
for (f = Math.ceil(fmin); f <= fmax; f++){
|
||||
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.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';
|
||||
for (db = Y_TOP; db >= Y_BOT; db -= 10){
|
||||
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.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.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();
|
||||
@@ -282,23 +480,24 @@
|
||||
}
|
||||
|
||||
function drawSpectrum(d){
|
||||
var P = PLOT();
|
||||
var fmin = d.f0, fmax = d.f0 + (d.n-1)*d.df;
|
||||
var m = drawGrid(fmin, fmax), i;
|
||||
|
||||
if (elChecked('spec_hold')){
|
||||
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];
|
||||
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;
|
||||
|
||||
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
|
||||
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.setLineDash([]);
|
||||
m.g.fillStyle = '#f87171'; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
|
||||
m.g.fillText('shoulder ' + d.shoulder.toFixed(1) + ' dB', m.l+6, y-3);
|
||||
m.g.fillStyle = P.marker; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
|
||||
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
|
||||
* 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 */
|
||||
if (false && eyeLastData && eyeLastData.err && eyeLastData.err.db){
|
||||
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++){
|
||||
var ef = er.f0 + ei*er.df;
|
||||
if (ef < fmin || ef > fmax){ started = false; continue; }
|
||||
@@ -498,12 +697,13 @@
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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="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_psip.html" title="TSID&PSIP">TSID&PSIP</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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -523,8 +723,15 @@
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -544,7 +751,8 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -570,16 +778,6 @@
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
|
||||
@@ -590,6 +788,16 @@
|
||||
<span class="summary-unit">dBc</span>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
@@ -670,27 +878,62 @@
|
||||
|
||||
|
||||
<main class="app-main">
|
||||
<h2>ORx Receive Spectrum</h2>
|
||||
<div class="box">
|
||||
<!-- PR12: Refresh/Pause/Peak-hold toolbar + status line removed on
|
||||
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 class="spec-tabs" role="tablist" aria-label="Spectrum display">
|
||||
<button type="button" class="spec-tab active" id="spectrum_tab" role="tab" aria-selected="true" onclick="selectSpectrumView('spectrum')">Output Spectrum</button>
|
||||
<button type="button" class="spec-tab" id="eye_tab" role="tab" aria-selected="false" onclick="selectSpectrumView('eye')">8VSB Eye Diagram</button>
|
||||
</div>
|
||||
|
||||
<h2>8VSB Eye Diagram</h2>
|
||||
<div class="box">
|
||||
<section class="spectrum-panel" id="spectrum_panel" role="tabpanel" aria-labelledby="spectrum_tab">
|
||||
<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…</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">> 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. -->
|
||||
<canvas id="eye_canvas"></canvas>
|
||||
<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 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>
|
||||
<!-- PR13: FSSI from the ORx FAGC gain index (255->0%, 195->100%).
|
||||
Value comes from the shared 3 s dashboard poll (dashboard.js),
|
||||
not from iq_eye.cgi. Shows "A / B" when FSSI refresh is on. -->
|
||||
<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="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>
|
||||
<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>
|
||||
<div class="spec-readout" title="Sampling phase offset."><span class="k">τ0</span><span class="v"><span id="rd_tau">--</span> sa</span></div>
|
||||
<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="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>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
+97
-31
@@ -4,7 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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/jquery.js"></script>
|
||||
<script src="js/dashboard.js"></script>
|
||||
@@ -58,10 +59,66 @@
|
||||
|
||||
var table = data.split("|");
|
||||
g_remot_local_ctrl = Number(table[0].split("#")[5]);
|
||||
document.title = table[0].split("#")[11];
|
||||
/* P-4b (2026-08-06): the title write here — document.title =
|
||||
table[0].split("#")[11] — is DELETED. On smod index 11 is ab_mode
|
||||
(the FSSI Refresh switch), not the web title. This page already sets
|
||||
the title from its web_title section below. Round 3 fixed only
|
||||
status_info; this is the rest of that class. */
|
||||
});
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
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() {
|
||||
// 1. DISABLE BUTTONS START (only in Remote mode)
|
||||
if(g_remot_local_ctrl == 1) {
|
||||
@@ -150,17 +207,11 @@
|
||||
var lpk_para = Number(tb_systs[ops]);
|
||||
ops++;
|
||||
|
||||
i = (lpk_para&0xff0000)>>16;
|
||||
if(i >= 2) i = 2;
|
||||
fcfg1.pktp.selectedIndex = i;
|
||||
|
||||
i = (lpk_para&0x00ff00)>>8;
|
||||
if(i >= 2) i = 2;
|
||||
fcfg1.pklen.selectedIndex = i;
|
||||
|
||||
i = (lpk_para&0x0000ff);
|
||||
if(i >= 2) i = 2;
|
||||
fcfg1.pkrec.selectedIndex = i;
|
||||
/* R4 (2026-08-11): the three clamps that parked an unrecognised firmware value on
|
||||
a phantom "Invalid" option are gone, and so is the option. See setEnumFromUnit. */
|
||||
setEnumFromUnit(fcfg1.pktp, (lpk_para&0xff0000)>>16);
|
||||
setEnumFromUnit(fcfg1.pklen, (lpk_para&0x00ff00)>>8);
|
||||
setEnumFromUnit(fcfg1.pkrec, (lpk_para&0x0000ff));
|
||||
|
||||
fcfg1.fec_swh.selectedIndex=Number(tb_systs[ops]);
|
||||
ops++;
|
||||
@@ -186,6 +237,7 @@
|
||||
// Remote mode: enable button
|
||||
$('#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();
|
||||
|
||||
@@ -514,12 +566,13 @@ function cmd_confirm() {
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<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="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_psip.html" title="TSID&PSIP">TSID&PSIP</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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
@@ -539,8 +592,15 @@ function cmd_confirm() {
|
||||
</button>
|
||||
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
|
||||
<h2 id="webtitle_header"><span></span></h2>
|
||||
<h3>
|
||||
<div class="dropdown">
|
||||
<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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
@@ -560,7 +620,8 @@ function cmd_confirm() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
@@ -586,16 +647,6 @@ function cmd_confirm() {
|
||||
<span class="summary-unit">dB</span>
|
||||
</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-label-row">
|
||||
<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>
|
||||
</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-label-row">
|
||||
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
|
||||
@@ -720,7 +781,7 @@ function cmd_confirm() {
|
||||
</tr>
|
||||
|
||||
<tr style="display: none;">
|
||||
<td>NET SPEED</td>
|
||||
<td>Net Speed</td>
|
||||
<td>
|
||||
<select name="eth_spd" id="eth_spd">
|
||||
<option value="0">100Mbps
|
||||
@@ -762,7 +823,6 @@ function cmd_confirm() {
|
||||
<select name="pktp" id="pktp">
|
||||
<option value="0">UDP
|
||||
<option value="1">RTP
|
||||
<option value="2">Invalid
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -772,7 +832,6 @@ function cmd_confirm() {
|
||||
<select name="pklen" id="pklen">
|
||||
<option value="0">188
|
||||
<option value="1">204
|
||||
<option value="2">Invalid
|
||||
</select>
|
||||
</td>
|
||||
<td>Type</td>
|
||||
@@ -780,7 +839,6 @@ function cmd_confirm() {
|
||||
<select name="pkrec" id="pkrec">
|
||||
<option value="0">Multicast
|
||||
<option value="1">Unicast
|
||||
<option value="2">Invalid
|
||||
</select>
|
||||
</td>
|
||||
</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>
|
||||
</tr>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user