🎉 Sync 2026-10-01 14:45:51

This commit is contained in:
github-actions[bot] committed 2026-10-01 14:45:51 +08:00
1 parent f1518a803c
commit 620e59fee5
124 files changed
+21594 -1

No files matched your search

Whitespace-only changes.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,339 @@
/* ==========================================================================
Dark Mode — luci-theme-glass
Compiled separately and injected by header.ut when dark mode is active.
========================================================================== */
:root {
/* ---- Colors — Dark mode ---- */
--color-primary: #0A84FF;
--color-primary-hover: #409cff;
--color-primary-active: #0066d6;
--color-primary-rgb: 10, 132, 255;
--color-success: #30D158;
--color-warning: #FFD60A;
--color-danger: #FF453A;
--color-info: #64D2FF;
--color-text: #f5f5f7;
--color-text-secondary: #98989d;
--color-text-tertiary: #636366;
--color-text-inverse: #1d1d1f;
--color-bg: #1c1c1e;
--color-bg-elevated: #2c2c2e;
--color-bg-secondary: #3a3a3c;
--color-border: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.16);
--color-separator: rgba(84, 84, 88, 0.36);
/* ---- Dark glassmorphism ---- */
--glass-blur: 25px;
--glass-saturate: 180%;
--glass-bg: rgba(38, 38, 42, 0.55);
--glass-bg-hover: rgba(52, 52, 56, 0.65);
--glass-bg-strong: rgba(38, 38, 42, 0.72);
--glass-border: rgba(255, 255, 255, 0.12);
--glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 0 1px rgba(0, 0, 0, 0.25);
--glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.15);
/* ---- Dark background ---- */
--bg-gradient: linear-gradient(135deg, #2c2e3a 0%, #353848 50%, #2a2d38 100%);
}
/* ---- Dark-mode-specific overrides ---- */
/* Inputs need darker backgrounds */
.cbi-input-text,
.cbi-input-password,
input[type="text"],
input[type="password"],
input[type="number"],
textarea,
select {
background: rgba(44, 44, 46, 0.55);
}
/* Login card */
.login-card {
background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
background-color: rgba(38, 38, 42, 0.55);
border-color: rgba(255, 255, 255, 0.14);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 16px rgba(255, 255, 255, 0.06);
}
.login-form .login-field input {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.04);
}
.login-btn {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
color: var(--color-text);
}
/* Buttons — glass-ui dark pattern */
.btn,
.cbi-button {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%);
background-color: transparent;
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
}
.btn:hover,
.cbi-button:hover {
opacity: 0.9;
background-color: transparent;
}
.btn:active,
.cbi-button:active {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2);
}
.cbi-button-negative,
.cbi-button-remove,
.cbi-button-reset {
background-image: linear-gradient(135deg, rgba(255, 69, 58, 0.12) 0%, rgba(255, 69, 58, 0.08) 50%, rgba(255, 69, 58, 0.1) 100%);
background-color: transparent;
color: var(--color-danger);
border-color: rgba(255, 69, 58, 0.25);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.08), inset 0 0 8px rgba(255, 255, 255, 0.06);
}
.cbi-button-negative:hover,
.cbi-button-remove:hover,
.cbi-button-reset:hover {
opacity: 0.9;
background-color: transparent;
}
.cbi-button-negative:active,
.cbi-button-remove:active,
.cbi-button-reset:active {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Sidebar */
#sidebar {
background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 20%, rgba(240, 248, 255, 0.08) 45%, rgba(255, 255, 255, 0.06) 70%, rgba(230, 240, 255, 0.07) 100%);
background-color: rgba(38, 38, 42, 0.55);
border-color: rgba(255, 255, 255, 0.14);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 12px rgba(255, 255, 255, 0.06);
}
/* Header bar */
#header {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
}
/* Page actions bar */
.cbi-page-actions {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
}
/* Nav hover & active */
.nav-item:hover {
background: rgba(255, 255, 255, 0.06);
}
.nav-item.active {
background: rgba(255, 255, 255, 0.15);
color: var(--color-text);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08);
}
/* Sub-item active — slider handles highlight */
.nav-sub .nav-item.active {
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* Sliding selector — dark glass */
.nav-sub .nav-slider {
background: rgba(255, 255, 255, 0.08);
border: 0.5px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.03);
}
/* Header tabs */
.header-tab:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--color-text);
}
.header-tab.active {
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* Tab slider — dark glass */
.tab-slider {
background: rgba(255, 255, 255, 0.1);
border: 0.5px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06);
}
/* Sub-nav bar */
#header-sub {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset;
}
.sub-tab:hover {
background: rgba(255, 255, 255, 0.06);
}
.sub-tab.active {
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* Header glass pills */
.header-title,
#status-indicators,
.header-actions {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.02) 50%, rgba(255, 255, 255, 0.03) 100%);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.12), inset 0 0.5px 0 rgba(255, 255, 255, 0.06);
}
/* Theme toggle */
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.08);
}
.theme-toggle-float {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 1px 6px rgba(255, 255, 255, 0.12);
}
/* Tabs */
.cbi-tabmenu {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15);
}
.cbi-tabmenu li a:hover {
background: rgba(255, 255, 255, 0.06);
}
.cbi-tabmenu li.cbi-tab a {
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08);
}
/* Section fold button */
.cbi-section.fade-in .cbi-title > div:last-child span:hover {
background: rgba(255, 255, 255, 0.08);
}
/* Table row hover */
table tbody tr:hover td,
.table .tr:hover .td {
background: rgba(255, 255, 255, 0.03);
}
/* Pre / code */
pre,
#syslog {
background: rgba(255, 255, 255, 0.04);
}
/* Alert colors adjusted for dark */
.alert-message,
.cbi-section-error {
background: rgba(100, 210, 255, 0.08);
border-color: rgba(100, 210, 255, 0.18);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04);
}
.alert-message.notice,
.alert-message.info {
background: rgba(100, 210, 255, 0.1);
border-color: rgba(100, 210, 255, 0.25);
}
.alert-message.warning {
background: rgba(255, 214, 10, 0.1);
border-color: rgba(255, 214, 10, 0.25);
}
.alert-message.error,
.cbi-section-error {
background: rgba(255, 69, 58, 0.1);
border-color: rgba(255, 69, 58, 0.25);
}
.alert-message.success {
background: rgba(48, 209, 88, 0.1);
border-color: rgba(48, 209, 88, 0.25);
}
/* Diagnostics & file browser in dark */
#diag-rc-output,
.cbi-filebrowser {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04);
}
/* Ifacebox zone header in dark — slightly brighter tint */
.ifacebox-head[style*="background-color"] {
background-color: rgba(var(--zone-color-rgb), 0.2) !important;
border-bottom-color: rgba(var(--zone-color-rgb), 0.25) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
/* Zone badges in dark — tint zone color instead of opaque background */
.zonebadge[style*="background-color"] {
background-color: rgba(var(--zone-color-rgb), 0.25) !important;
border-color: rgba(var(--zone-color-rgb), 0.3);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
}
.zonebadge {
border-color: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
}
/* Ifacebox in dark */
.ifacebadge {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03);
}
/* Port grid boxes in dark — glass-ui dark card */
div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox {
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%) !important;
border-color: rgba(255, 255, 255, 0.15) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset !important;
}
/* Network status table badges in dark */
.network-status-table .ifacebox-body .ifacebadge {
background: rgba(255, 255, 255, 0.06);
}
/* Dropdown open items hover */
.cbi-dropdown[open] > ul.dropdown > li:hover {
background: rgba(255, 255, 255, 0.06);
}
/* Progressbar */
.cbi-progressbar {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.06);
}
.cbi-progressbar > div {
background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.65) 0%, rgba(var(--color-primary-rgb), 0.5) 50%, rgba(var(--color-primary-rgb), 0.6) 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 0 6px rgba(255, 255, 255, 0.06);
}
.cbi-progressbar::after {
color: var(--color-text);
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
}
/* Ifacebox-body itemlist labels */
.ifacebox-body > span > .nowrap > strong {
color: var(--color-text-tertiary);
}
/* UCI Changes dialog in dark */
.uci-change-legend-label > ins {
background-color: rgba(48, 209, 88, 0.3);
border-color: rgba(48, 209, 88, 0.5);
}
.uci-change-legend-label > del {
background-color: rgba(255, 69, 58, 0.3);
border-color: rgba(255, 69, 58, 0.5);
}
.uci-change-legend-label > var {
background-color: rgba(100, 210, 255, 0.25);
border-color: rgba(100, 210, 255, 0.4);
}
.uci-change-list ins {
background-color: rgba(48, 209, 88, 0.12);
border-color: rgba(48, 209, 88, 0.25);
}
.uci-change-list del {
background-color: rgba(255, 69, 58, 0.12);
border-color: rgba(255, 69, 58, 0.25);
}
.uci-change-list var {
background-color: rgba(100, 210, 255, 0.1);
border-color: rgba(100, 210, 255, 0.18);
}
/* Selection */
::selection {
background: rgba(var(--color-primary-rgb), 0.35);
}
@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#F5F5F7"/>
<stop offset="100%" stop-color="#E8E8ED"/>
</linearGradient>
<linearGradient id="glass" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="white" stop-opacity="0.9"/>
<stop offset="40%" stop-color="white" stop-opacity="0.4"/>
<stop offset="100%" stop-color="white" stop-opacity="0.6"/>
</linearGradient>
<linearGradient id="edge" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="white" stop-opacity="0.95"/>
<stop offset="50%" stop-color="#D1D1D6" stop-opacity="0.4"/>
<stop offset="100%" stop-color="white" stop-opacity="0.7"/>
</linearGradient>
<radialGradient id="refract" cx="18" cy="16" r="36" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="white" stop-opacity="0.55"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</radialGradient>
<!-- 3D shadow filter for WiFi arcs -->
<filter id="arc-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" flood-color="#002266" flood-opacity="0.3"/>
</filter>
<!-- Arc stroke gradients — darker Apple blue with depth -->
<linearGradient id="arc1" x1="14" y1="26" x2="50" y2="30" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#3A8EF6" stop-opacity="0.3"/>
<stop offset="20%" stop-color="#0040DD" stop-opacity="0.9"/>
<stop offset="50%" stop-color="#0033BB" stop-opacity="1"/>
<stop offset="80%" stop-color="#0040DD" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.3"/>
</linearGradient>
<linearGradient id="arc2" x1="20" y1="32" x2="44" y2="36" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#3A8EF6" stop-opacity="0.25"/>
<stop offset="25%" stop-color="#0040DD" stop-opacity="0.8"/>
<stop offset="50%" stop-color="#0033BB" stop-opacity="0.95"/>
<stop offset="75%" stop-color="#0040DD" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.25"/>
</linearGradient>
<linearGradient id="arc3" x1="26" y1="38" x2="38" y2="42" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#4D9BF7" stop-opacity="0.2"/>
<stop offset="30%" stop-color="#0040DD" stop-opacity="0.7"/>
<stop offset="50%" stop-color="#0033BB" stop-opacity="0.9"/>
<stop offset="70%" stop-color="#0040DD" stop-opacity="0.7"/>
<stop offset="100%" stop-color="#4D9BF7" stop-opacity="0.2"/>
</linearGradient>
<radialGradient id="dot-grad" cx="32" cy="46" r="3.5" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#0033BB"/>
<stop offset="60%" stop-color="#0040DD"/>
<stop offset="100%" stop-color="#3A8EF6" stop-opacity="0.4"/>
</radialGradient>
</defs>
<!-- Outer shadow for 3D depth -->
<rect width="64" height="64" rx="14" fill="none" filter="url(#arc-shadow)"/>
<!-- Light background -->
<rect width="64" height="64" rx="14" fill="url(#bg)"/>
<!-- Clean glass pane -->
<rect x="4" y="4" width="56" height="56" rx="12" fill="url(#glass)" stroke="url(#edge)" stroke-width="0.75"/>
<!-- Light refraction highlight -->
<rect x="4" y="4" width="56" height="28" rx="12" fill="url(#refract)"/>
<!-- Inner glow edge -->
<rect x="5" y="5" width="54" height="54" rx="11" fill="none" stroke="white" stroke-opacity="0.35" stroke-width="0.5"/>
<!-- WiFi shadow layer (dark offset below — 3D depth) -->
<path d="M14 30a25.5 25.5 0 0 1 36 0" fill="none" stroke="#002266" stroke-opacity="0.18" stroke-width="3" stroke-linecap="round"/>
<path d="M20 36a16 16 0 0 1 24 0" fill="none" stroke="#002266" stroke-opacity="0.15" stroke-width="3" stroke-linecap="round"/>
<path d="M26 42a7.5 7.5 0 0 1 12 0" fill="none" stroke="#002266" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<!-- WiFi highlight layer (white offset above — raised/embossed) -->
<path d="M14 27a25.5 25.5 0 0 1 36 0" fill="none" stroke="white" stroke-opacity="0.65" stroke-width="2" stroke-linecap="round"/>
<path d="M20 33a16 16 0 0 1 24 0" fill="none" stroke="white" stroke-opacity="0.6" stroke-width="2" stroke-linecap="round"/>
<path d="M26 39a7.5 7.5 0 0 1 12 0" fill="none" stroke="white" stroke-opacity="0.55" stroke-width="2" stroke-linecap="round"/>
<!-- WiFi main arcs — darker blue gradient -->
<path d="M14 28a25.5 25.5 0 0 1 36 0" fill="none" stroke="url(#arc1)" stroke-width="2.5" stroke-linecap="round"/>
<path d="M20 34a16 16 0 0 1 24 0" fill="none" stroke="url(#arc2)" stroke-width="2.5" stroke-linecap="round"/>
<path d="M26 40a7.5 7.5 0 0 1 12 0" fill="none" stroke="url(#arc3)" stroke-width="2.5" stroke-linecap="round"/>
<!-- Center dot with 3D -->
<circle cx="32" cy="48" r="2.5" fill="#002266" fill-opacity="0.15"/>
<circle cx="32" cy="46" r="2.5" fill="url(#dot-grad)"/>
<circle cx="31.2" cy="45.2" r="0.9" fill="white" fill-opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,327 @@
'use strict';
'require baseclass';
'require ui';
return baseclass.extend({
__init__: function() {
ui.menu.load().then(L.bind(this.render, this));
},
/* Icon SVG paths for top-level sidebar categories (stroke-based, 24x24 viewBox) */
icons: {
'status': '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
'system': '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09a1.65 1.65 0 00-1-1.51 1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09a1.65 1.65 0 001.51-1 1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33h.09a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82v.09a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
'network': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
'services': '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
'nas': '<line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/>',
'vpn': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>',
'docker': '<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
'logout': '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
'_default': '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>'
},
getIcon: function(name) {
var pathData = this.icons[name] || this.icons['_default'];
var markup = '<svg xmlns="http://www.w3.org/2000/svg" class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">' + pathData + '</svg>';
var doc = new DOMParser().parseFromString(markup, 'image/svg+xml');
return document.importNode(doc.documentElement, true);
},
render: function(tree) {
var container = document.getElementById('sidebar-nav');
if (!container) return;
/* Clear existing menu elements to prevent duplicates on re-render (e.g. Save & Apply) */
container.innerHTML = '';
var headerNav = document.getElementById('header-nav');
if (headerNav) {
/* Remove only tabs and sliders, preserve header-title */
var old = headerNav.querySelectorAll('.header-tab, .tab-slider');
for (var k = 0; k < old.length; k++) old[k].remove();
}
var headerTabs = document.getElementById('header-tabs');
if (headerTabs) headerTabs.innerHTML = '';
var topChildren = ui.menu.getChildren(tree);
for (var i = 0; i < topChildren.length; i++) {
var topNode = topChildren[i];
var isTopActive = L.env.requestpath.length
? topNode.name == L.env.requestpath[0]
: i == 0;
if (isTopActive) {
this.renderSidebarMenu(container, topNode, topNode.name);
}
}
/* Render mode menu (top-level tabs) if more than one category */
if (topChildren.length > 1) {
this.renderModeMenu(topChildren);
}
/* Set header title from the deepest active menu node */
var titleEl = document.getElementById('header-title');
if (titleEl) {
var node = tree;
var title = '';
for (var i = 0; i < L.env.dispatchpath.length && node; i++) {
node = node.children[L.env.dispatchpath[i]];
if (node && node.title)
title = node.title;
}
titleEl.textContent = title ? _(title) : '';
}
/* Render tab menu for sub-sub pages */
if (L.env.dispatchpath.length >= 3) {
var node = tree;
var url = '';
for (var i = 0; i < 3 && node; i++) {
node = node.children[L.env.dispatchpath[i]];
url = url + (url ? '/' : '') + L.env.dispatchpath[i];
}
if (node)
this.renderTabMenu(node, url);
}
},
/* Shared: position a slider behind the active tab element */
positionTabSlider: function(container, slider, activeEl, animate) {
if (!activeEl) return;
if (!animate) slider.style.transition = 'none';
slider.style.left = activeEl.offsetLeft + 'px';
slider.style.width = activeEl.offsetWidth + 'px';
slider.style.opacity = '1';
if (!animate) {
requestAnimationFrame(function() {
slider.style.transition = '';
});
}
},
/* Shared: add slider to a horizontal tab container */
addTabSlider: function(container, navigateOnClick) {
var slider = E('div', { 'class': 'tab-slider' });
container.insertBefore(slider, container.firstChild);
var self = this;
/* Attach click handlers to all tabs */
var tabs = container.querySelectorAll('.header-tab, .sub-tab');
for (var i = 0; i < tabs.length; i++) {
(function(tab, sl) {
tab.addEventListener('click', function(ev) {
if (navigateOnClick) {
/* Slide first, then navigate */
ev.preventDefault();
var prev = container.querySelector('.header-tab.active, .sub-tab.active');
if (prev) prev.classList.remove('active');
tab.classList.add('active');
self.positionTabSlider(container, sl, tab, true);
document.body.classList.add('page-leaving');
var href = tab.href;
setTimeout(function() {
window.location.href = href;
}, 180);
} else {
/* In-page tab switch — just move slider */
self.positionTabSlider(container, sl, tab, true);
}
});
})(tabs[i], slider);
}
/* Position on active tab */
requestAnimationFrame(function() {
var active = container.querySelector('.header-tab.active, .sub-tab.active');
self.positionTabSlider(container, slider, active, false);
});
return slider;
},
renderModeMenu: function(topChildren) {
var container = document.getElementById('header-nav');
if (!container) return;
for (var i = 0; i < topChildren.length; i++) {
var child = topChildren[i];
var isActive = L.env.requestpath.length
? child.name == L.env.requestpath[0]
: i == 0;
var item = E('a', {
'class': 'header-tab' + (isActive ? ' active' : ''),
'href': L.url(child.name),
'data-name': child.name
}, [ _(child.title) ]);
container.appendChild(item);
}
/* Add sliding selector */
this.addTabSlider(container, true);
},
renderSidebarMenu: function(container, tree, url) {
var children = ui.menu.getChildren(tree);
if (children.length == 0) return;
var group = E('div', { 'class': 'nav-group' });
for (var i = 0; i < children.length; i++) {
var child = children[i];
var subChildren = ui.menu.getChildren(child);
var hasChildren = subChildren.length > 0;
var isActive = L.env.dispatchpath[1] == child.name;
/* For items with children, link to first child */
var linkUrl = hasChildren
? L.url(url, child.name, subChildren[0].name)
: L.url(url, child.name);
var item = E('a', {
'class': 'nav-item' + (isActive ? ' open' : ''),
'href': linkUrl,
'data-name': child.name
}, [
E('span', { 'class': 'nav-label' }, [ _(child.title) ])
]);
/* Add icon before the label */
item.insertBefore(this.getIcon(child.name), item.firstChild);
if (hasChildren) {
item.appendChild(E('svg', {
'class': 'nav-arrow',
'viewBox': '0 0 24 24',
'fill': 'none',
'stroke': 'currentColor',
'stroke-width': '2'
}, [
E('polyline', { 'points': '9 18 15 12 9 6' })
]));
}
group.appendChild(item);
if (hasChildren) {
var subMenu = E('div', {
'class': 'nav-sub' + (isActive ? ' open' : '')
});
/* Sliding highlight indicator */
var slider = E('div', { 'class': 'nav-slider' });
subMenu.appendChild(slider);
for (var j = 0; j < subChildren.length; j++) {
var sub = subChildren[j];
var isSubActive = isActive && L.env.dispatchpath[2] == sub.name;
var subItem = E('a', {
'class': 'nav-item' + (isSubActive ? ' active' : ''),
'href': L.url(url, child.name, sub.name)
}, [
E('span', { 'class': 'nav-label' }, [ _(sub.title) ])
]);
/* Slide highlight to clicked item before navigating */
(function(el, sl) {
el.addEventListener('click', function(ev) {
ev.preventDefault();
var prev = el.parentNode.querySelector('.nav-item.active');
if (prev) prev.classList.remove('active');
el.classList.add('active');
sl.style.top = el.offsetTop + 'px';
sl.style.height = el.offsetHeight + 'px';
sl.style.opacity = '1';
document.body.classList.add('page-leaving');
setTimeout(function() {
window.location.href = el.href;
}, 180);
});
})(subItem, slider);
subMenu.appendChild(subItem);
}
/* Position slider on the active item after render */
(function(sm, sl) {
requestAnimationFrame(function() {
var active = sm.querySelector('.nav-item.active');
if (active) {
sl.style.transition = 'none';
sl.style.top = active.offsetTop + 'px';
sl.style.height = active.offsetHeight + 'px';
sl.style.opacity = '1';
/* Re-enable transition after initial position */
requestAnimationFrame(function() {
sl.style.transition = '';
});
}
});
})(subMenu, slider);
group.appendChild(subMenu);
/* Bind click handler to toggle submenu */
(function(navItem, subEl) {
navItem.addEventListener('click', function(ev) {
ev.preventDefault();
var isOpen = subEl.classList.contains('open');
if (isOpen) {
subEl.classList.remove('open');
navItem.classList.remove('open');
} else {
subEl.classList.add('open');
navItem.classList.add('open');
}
});
})(item, subMenu);
}
}
container.appendChild(group);
},
renderTabMenu: function(tree, url, level) {
var container = document.getElementById('header-sub');
if (!container) return E([]);
var children = ui.menu.getChildren(tree);
var activeNode = null;
if (children.length == 0)
return E([]);
/* Clear any CBI tabs that footer.ut added before menu loaded */
container.innerHTML = '';
container.classList.remove('active');
document.body.classList.remove('has-sub-nav');
for (var i = 0; i < children.length; i++) {
var child = children[i];
var isActive = L.env.dispatchpath[3 + (level || 0)] == child.name;
container.appendChild(E('a', {
'class': 'sub-tab' + (isActive ? ' active' : ''),
'href': L.url(url, child.name)
}, [ _(child.title) ]));
if (isActive)
activeNode = child;
}
container.classList.add('active');
document.body.classList.add('has-sub-nav');
/* Add sliding selector */
this.addTabSlider(container, true);
/* Do NOT recurse — deeper menu-tree levels stay in-content as CBI tabmenus */
}
});
@@ -0,0 +1,286 @@
'use strict';
'require baseclass';
'require rpc';
'require fs';
var callSystemInfo = rpc.declare({
object: 'system',
method: 'info'
});
var callInterfaceDump = rpc.declare({
object: 'network.interface',
method: 'dump',
expect: { 'interface': [] }
});
var callDeviceStatus = rpc.declare({
object: 'network.device',
method: 'status',
params: ['name']
});
return baseclass.extend({
prevStats: null,
prevTime: null,
netDevice: null,
netLabel: null,
netChecked: false,
linkSpeed: null,
numCores: 1,
__init__: function() {
var self = this;
/* Detect core count once so load average can be converted to %. */
L.resolveDefault(fs.read('/proc/cpuinfo'), '').then(function(text) {
var m = text.match(/^processor\s*:/gm);
self.numCores = (m && m.length) || 1;
});
this.setupIndicators();
},
icons: {
cpu: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 9 4 4 7 11 10 2 13 9"/></svg>',
ram: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M1 3 H13 V10 H8 V9 H6 V10 H1 Z"/><rect x="2.2" y="4.5" width="2" height="3"/><rect x="6" y="4.5" width="2" height="3"/><rect x="9.8" y="4.5" width="2" height="3"/><line x1="2" y1="10" x2="2" y2="11.5"/><line x1="4" y1="10" x2="4" y2="11.5"/><line x1="10" y1="10" x2="10" y2="11.5"/><line x1="12" y1="10" x2="12" y2="11.5"/></svg>',
net: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="1" x2="7" y2="13"/><polyline points="3 4 7 1 11 4"/><polyline points="3 10 7 13 11 10"/></svg>',
uptime: '<svg xmlns="http://www.w3.org/2000/svg" class="indicator-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="7" cy="7" r="6"/><polyline points="7 3 7 7 10 9"/></svg>'
},
makeIcon: function(name) {
var markup = this.icons[name];
if (!markup) return document.createTextNode('');
var doc = new DOMParser().parseFromString(markup, 'image/svg+xml');
return document.importNode(doc.documentElement, true);
},
createIndicator: function(name, title) {
var el = document.createElement('span');
el.setAttribute('data-indicator', name);
el.title = title;
el.appendChild(this.makeIcon(name));
var val = document.createElement('span');
val.className = 'indicator-value';
/* Restore last known value from session to avoid flash of "--" on page nav */
var cached = null;
try { cached = sessionStorage.getItem('glass-status-' + name); } catch(e) {}
val.textContent = cached || '--';
el.appendChild(val);
return el;
},
setLevel: function(el, level) {
el.setAttribute('data-level', level);
},
setupIndicators: function() {
this.container = document.getElementById('status-indicators');
if (!this.container) return;
this.cpuEl = this.createIndicator('cpu', 'CPU Load');
this.ramEl = this.createIndicator('ram', 'Memory Usage');
this.uptimeEl = this.createIndicator('uptime', 'Uptime');
this.container.appendChild(this.cpuEl);
this.container.appendChild(this.ramEl);
this.container.appendChild(this.uptimeEl);
this.fetchAndUpdate();
setInterval(L.bind(this.fetchAndUpdate, this), 5000);
},
fetchAndUpdate: function() {
var self = this;
L.resolveDefault(callSystemInfo(), {}).then(function(info) {
self.updateSystem(info);
});
if (!this.netChecked) {
this.netChecked = true;
L.resolveDefault(callInterfaceDump(), []).then(function(ifaces) {
/* Find WAN device — prefer 'wan' over 'wan6' */
var wanDev = null;
for (var i = 0; i < ifaces.length; i++) {
var iface = ifaces[i];
if (iface.interface === 'wan') {
wanDev = iface.l3_device || iface.device || null;
break;
}
}
if (!wanDev) {
for (var i = 0; i < ifaces.length; i++) {
if (ifaces[i].interface === 'wan6') {
wanDev = ifaces[i].l3_device || ifaces[i].device || null;
break;
}
}
}
self.resolvePhysicalDevice(wanDev || 'wan');
});
} else if (this.netDevice) {
this.pollNetwork();
}
},
resolvePhysicalDevice: function(devName) {
var self = this;
L.resolveDefault(callDeviceStatus(devName), {}).then(function(dev) {
if (!dev) return;
var members = dev['bridge-members'];
if (dev.type === 'bridge' && members && members.length > 0) {
/* Bridge stats only count management traffic;
use first physical port for real forwarded stats */
self.netDevice = members[0];
} else if (dev.statistics) {
self.netDevice = devName;
}
if (!self.netDevice) return;
/* Query physical port for link speed and DSA info */
L.resolveDefault(callDeviceStatus(self.netDevice), {}).then(function(phys) {
if (phys && phys.speed) {
var m = String(phys.speed).match(/^(\d+)/);
if (m) self.linkSpeed = parseInt(m[1], 10);
}
/* DSA port with hardware offload: per-port counters miss
offloaded flows; use the conduit (switch master) instead */
if (phys && phys.devtype === 'dsa' && phys['hw-tc-offload'] && phys.conduit) {
self.netLabel = self.netDevice;
self.netDevice = phys.conduit;
}
self.netEl = self.createIndicator('net', 'Throughput');
self.container.insertBefore(self.netEl, self.uptimeEl);
self.pollNetwork();
});
});
},
pollNetwork: function() {
var self = this;
L.resolveDefault(callDeviceStatus(this.netDevice), {}).then(function(dev) {
self.updateNetwork(dev);
});
},
updateSystem: function(info) {
if (!info) return;
if (info.load) {
var load1 = info.load[0] / 65536;
var load5 = (info.load[1] / 65536).toFixed(2);
var load15 = (info.load[2] / 65536).toFixed(2);
var pct = Math.min(load1 / this.numCores, 1) * 100;
var pctStr = pct.toFixed(0) + '%';
this.cpuEl.querySelector('.indicator-value').textContent = pctStr;
try { sessionStorage.setItem('glass-status-cpu', pctStr); } catch(e) {}
this.cpuEl.title = 'CPU: ' + pctStr + ' (load ' + load1.toFixed(2) +
' / ' + load5 + ' / ' + load15 + ' on ' + this.numCores +
(this.numCores === 1 ? ' core)' : ' cores)');
var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit';
this.setLevel(this.cpuEl, level);
}
if (info.memory) {
var total = info.memory.total;
var avail = info.memory.available || info.memory.free;
var used = total - avail;
var pct = (used / total * 100).toFixed(0);
this.ramEl.querySelector('.indicator-value').textContent = pct + '%';
try { sessionStorage.setItem('glass-status-ram', pct + '%'); } catch(e) {}
this.ramEl.title = 'RAM: ' + this.formatBytes(used) + ' / ' + this.formatBytes(total) + ' (' + pct + '% used)';
var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit';
this.setLevel(this.ramEl, level);
}
if (info.uptime) {
this.uptimeEl.querySelector('.indicator-value').textContent = this.formatUptime(info.uptime);
try { sessionStorage.setItem('glass-status-uptime', this.formatUptime(info.uptime)); } catch(e) {}
this.uptimeEl.title = 'Uptime: ' + this.formatUptimeFull(info.uptime);
}
},
updateNetwork: function(dev) {
if (!dev || !dev.statistics || !this.netEl) return;
var now = Date.now() / 1000;
var rx = dev.statistics.rx_bytes || 0;
var tx = dev.statistics.tx_bytes || 0;
if (this.prevStats && this.prevTime) {
var dt = now - this.prevTime;
if (dt > 0) {
var rxSpeed = (rx - this.prevStats.rx) / dt;
var txSpeed = (tx - this.prevStats.tx) / dt;
if (rxSpeed < 0) rxSpeed = 0;
if (txSpeed < 0) txSpeed = 0;
var netText = '\u2193' + this.formatSpeed(rxSpeed) + ' \u2191' + this.formatSpeed(txSpeed);
this.netEl.querySelector('.indicator-value').textContent = netText;
try { sessionStorage.setItem('glass-status-net', netText); } catch(e) {}
var tip = (this.netLabel || this.netDevice) + ': \u2193 ' + this.formatSpeedFull(rxSpeed) + ' / \u2191 ' + this.formatSpeedFull(txSpeed);
if (this.linkSpeed)
tip += ' (Link: ' + (this.linkSpeed >= 1000 ? (this.linkSpeed / 1000) + ' Gbps' : this.linkSpeed + ' Mbps') + ')';
this.netEl.title = tip;
var peak = Math.max(rxSpeed, txSpeed);
var level = peak < 1048576 ? 'ok' : peak < 52428800 ? 'active' : 'busy';
this.setLevel(this.netEl, level);
}
}
this.prevStats = { rx: rx, tx: tx };
this.prevTime = now;
},
formatUptime: function(s) {
var d = Math.floor(s / 86400);
var h = Math.floor((s % 86400) / 3600);
var m = Math.floor((s % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
return h + 'h ' + m + 'm';
},
formatUptimeFull: function(s) {
var d = Math.floor(s / 86400);
var h = Math.floor((s % 86400) / 3600);
var m = Math.floor((s % 3600) / 60);
var parts = [];
if (d > 0) parts.push(d + (d === 1 ? ' day' : ' days'));
if (h > 0) parts.push(h + (h === 1 ? ' hour' : ' hours'));
if (m > 0) parts.push(m + (m === 1 ? ' min' : ' mins'));
return parts.join(', ') || '< 1 min';
},
formatBytes: function(bytes) {
if (bytes >= 1073741824)
return (bytes / 1073741824).toFixed(1) + ' GB';
if (bytes >= 1048576)
return (bytes / 1048576).toFixed(0) + ' MB';
return (bytes / 1024).toFixed(0) + ' KB';
},
formatSpeed: function(bytesPerSec) {
var bits = bytesPerSec * 8;
if (bits >= 1000000000)
return (bits / 1000000000).toFixed(1) + 'G';
if (bits >= 1000000)
return (bits / 1000000).toFixed(1) + 'M';
if (bits >= 1000)
return (bits / 1000).toFixed(1) + 'K';
return Math.round(bits) + 'b';
},
formatSpeedFull: function(bytesPerSec) {
var bits = bytesPerSec * 8;
if (bits >= 1000000000)
return (bits / 1000000000).toFixed(1) + ' Gbps';
if (bits >= 1000000)
return (bits / 1000000).toFixed(1) + ' Mbps';
if (bits >= 1000)
return (bits / 1000).toFixed(1) + ' kbps';
return Math.round(bits) + ' bps';
}
});
@@ -0,0 +1,137 @@
'use strict';
'require view';
'require form';
'require uci';
'require fs';
return view.extend({
load: function () {
return Promise.all([
uci.load('glass'),
L.resolveDefault(fs.list('/www/luci-static/glass/background'), [])
]);
},
render: function (data) {
var bgFiles = (data[1] || []).filter(function(f) {
return f.name && !f.name.startsWith('.') && /\.(jpe?g|png|gif|webp|mp4|webm)$/i.test(f.name);
});
var hasBg = bgFiles.length > 0;
var bgName = hasBg ? bgFiles[0].name : null;
var m, s, o;
m = new form.Map('glass', _('Glass Theme'),
_('Configure the appearance of the Glass theme. Changes take effect after saving and refreshing the page.'));
/* ── General ── */
s = m.section(form.NamedSection, 'global', 'global', _('General'));
s.anonymous = true;
o = s.option(form.ListValue, 'mode', _('Theme mode'),
_('Controls the overall appearance. "Normal" follows the system preference.'));
o.value('normal', _('Auto (follow system)'));
o.value('light', _('Light'));
o.value('dark', _('Dark'));
o.default = 'normal';
o = s.option(form.Flag, 'status_bar', _('Header status bar'),
_('Show live system stats (CPU, RAM, network, uptime) in the header. Disable to reduce resource usage on low-end devices.'));
o.default = '1';
o.rmempty = false;
o = s.option(form.ListValue, 'font_size', _('Base font size'),
_('Scales all text in the theme. Larger values improve readability on high-DPI displays.'));
o.value('13', _('Small (13px)'));
o.value('14', _('Normal (14px)'));
o.value('16', _('Large (16px)'));
o.value('18', _('Extra Large (18px)'));
o.default = '14';
/* ── Colors ── */
s = m.section(form.NamedSection, 'global', 'global', _('Accent Colors'));
s.anonymous = true;
o = s.option(form.Value, 'primary', _('Primary color (light mode)'),
_('Accent color used for active elements, links, and buttons in light mode.'));
o.default = '#007AFF';
o.placeholder = '#007AFF';
o.datatype = 'string';
o.renderWidget = function(section_id, option_index, cfgvalue) {
var el = form.Value.prototype.renderWidget.apply(this, arguments);
var input = el.querySelector('input');
if (input) {
input.type = 'color';
input.style.height = '2.5rem';
input.style.cursor = 'pointer';
}
return el;
};
o = s.option(form.Value, 'dark_primary', _('Primary color (dark mode)'),
_('Accent color used for active elements, links, and buttons in dark mode.'));
o.default = '#0A84FF';
o.placeholder = '#0A84FF';
o.datatype = 'string';
o.renderWidget = function(section_id, option_index, cfgvalue) {
var el = form.Value.prototype.renderWidget.apply(this, arguments);
var input = el.querySelector('input');
if (input) {
input.type = 'color';
input.style.height = '2.5rem';
input.style.cursor = 'pointer';
}
return el;
};
/* ── Glass Effects (Light Mode) ── */
s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Light Mode'));
s.anonymous = true;
o = s.option(form.Value, 'blur', _('Blur intensity (px)'),
_('Backdrop blur radius for glass panels. Higher values create a more frosted appearance.'));
o.default = '20';
o.placeholder = '20';
o.datatype = 'range(0,50)';
o = s.option(form.Value, 'transparency', _('Panel transparency'),
_('Background opacity of glass panels (0 = fully transparent, 1 = fully opaque).'));
o.default = '0.72';
o.placeholder = '0.72';
o.datatype = 'string';
/* ── Glass Effects (Dark Mode) ── */
s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Dark Mode'));
s.anonymous = true;
o = s.option(form.Value, 'blur_dark', _('Blur intensity (px)'),
_('Backdrop blur radius for glass panels in dark mode.'));
o.default = '25';
o.placeholder = '25';
o.datatype = 'range(0,50)';
o = s.option(form.Value, 'transparency_dark', _('Panel transparency'),
_('Background opacity of glass panels in dark mode (0 = fully transparent, 1 = fully opaque).'));
o.default = '0.30';
o.placeholder = '0.30';
o.datatype = 'string';
/* ── Background ── */
s = m.section(form.NamedSection, 'global', 'global', _('Background'));
s.anonymous = true;
o = s.option(form.DummyValue, '_bg_info', _('Current background'));
o.rawhtml = true;
o.cfgvalue = function () {
if (hasBg) {
return '<span style="color:var(--color-success)">' +
_('Active') + ': ' + bgName + '</span>';
}
return '<span style="color:var(--color-text-secondary)">' +
_('No background set. Upload an image or video named "bg" (e.g. bg.jpg, bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP.') +
'</span>';
};
return m.render();
}
});