Add dashboard theme settings and contrast tests

This commit is contained in:
Luna 2026-06-16 02:29:45 -07:00
parent 7f4d5b42fd
commit bfef23fc4a
7 changed files with 424 additions and 53 deletions

View file

@ -5,45 +5,190 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Enodia Sentinel</title>
<script>
(function(){
try{
var theme = localStorage.getItem("sentinel.theme") || "console";
document.documentElement.dataset.theme = theme;
}catch(_e){ document.documentElement.dataset.theme = "console"; }
})();
</script>
<style>
:root{
--bg:#0a0c0f; --ink:#d8dee7; --muted:#8590a3; --soft:#aeb7c7;
--panel:#11151b; --panel2:#151b23; --rail:#0d1117; --line:#26313f;
--accent:#22c7a9; --amber:#f2b84b; --red:#f06476; --blue:#61a8ff;
--violet:#b68cff; --copper:#d8925b; --ok:#56d364;
--violet:#b68cff; --copper:#d8925b; --ok:#56d364; --white:#fff;
--header-bg:rgba(13,17,23,.96); --aside-bg:rgba(13,17,23,.9);
--work-bg:linear-gradient(180deg,#0f1319,#0b0d11);
--plan-bg:rgba(17,21,27,.92); --code-bg:#080a0d;
--button-bg:var(--panel2); --active-ink:#00110e; --focus:#f7d86a;
--grid-a:rgba(255,255,255,.06); --grid-b:rgba(255,255,255,.05);
--signal:linear-gradient(90deg,var(--accent),var(--blue),var(--copper),var(--red));
--shadow:0 18px 48px rgba(0,0,0,.28);
}
:root[data-theme="paper"]{
--bg:#edf2f7; --ink:#1b2633; --muted:#627084; --soft:#33445a;
--panel:#ffffff; --panel2:#f7fafc; --rail:#e4ebf3; --line:#becada;
--accent:#167b83; --amber:#a65f13; --red:#b43449; --blue:#2764ba;
--violet:#6f56b8; --copper:#8b542c; --ok:#247c45; --white:#07111c;
--header-bg:rgba(248,251,254,.98); --aside-bg:rgba(232,239,247,.94);
--work-bg:linear-gradient(180deg,#f8fbfe,#edf2f7);
--plan-bg:rgba(245,248,252,.96); --code-bg:#e8eef5;
--button-bg:#ffffff; --active-ink:#ffffff; --focus:#7b3ff3;
--grid-a:rgba(11,31,52,.08); --grid-b:rgba(11,31,52,.06);
--signal:linear-gradient(90deg,#167b83,#2764ba,#8b542c,#b43449);
--shadow:0 18px 34px rgba(30,46,64,.12);
}
:root[data-theme="contrast"]{
--bg:#000; --ink:#f4f7fb; --muted:#c3ccd8; --soft:#e1e7ef;
--panel:#101010; --panel2:#181818; --rail:#060606; --line:#f4f7fb;
--accent:#00e5ff; --amber:#ffd84d; --red:#ff4f69; --blue:#7db3ff;
--violet:#d5a3ff; --copper:#ffb15f; --ok:#74ff91; --white:#fff;
--header-bg:#000; --aside-bg:#050505; --work-bg:#000; --plan-bg:#090909;
--code-bg:#000; --button-bg:#121212; --active-ink:#000; --focus:#ffd84d;
--grid-a:rgba(255,255,255,.11); --grid-b:rgba(255,255,255,.09);
--signal:linear-gradient(90deg,#00e5ff,#f4f7fb,#ffd84d,#ff4f69);
--shadow:none;
}
:root[data-theme="pride"]{
--bg:#111018; --ink:#faf8ff; --muted:#b8b1c9; --soft:#e0d9ef;
--panel:#1b1924; --panel2:#252232; --rail:#16131f; --line:#3d3651;
--accent:#ff8c00; --amber:#ffed00; --red:#ff5f6d; --blue:#73b7ff;
--violet:#b98cff; --copper:#ff8c00; --ok:#00c853; --white:#fff;
--header-bg:rgba(17,16,24,.97); --aside-bg:rgba(22,19,31,.94);
--work-bg:linear-gradient(180deg,#1a1724,#111018);
--plan-bg:rgba(27,25,36,.95); --code-bg:#0d0c12;
--button-bg:#252232; --active-ink:#111018; --focus:#ffed00;
--grid-a:rgba(255,255,255,.07); --grid-b:rgba(255,255,255,.05);
--signal:linear-gradient(90deg,#e40303,#ff8c00,#ffed00,#008026,#24408e,#732982);
--shadow:0 18px 46px rgba(0,0,0,.32);
}
:root[data-theme="trans"]{
--bg:#55CDFC; --ink:#000000; --muted:#000000; --soft:#000000;
--panel:#FFFFFF; --panel2:#F7A8B8; --rail:#55CDFC; --line:#F7A8B8;
--accent:#55CDFC; --amber:#000000; --red:#000000; --blue:#000000;
--violet:#F7A8B8; --copper:#F7A8B8; --ok:#000000; --white:#000000;
--header-bg:linear-gradient(90deg,#55CDFC 0 20%,#F7A8B8 20% 40%,#FFFFFF 40% 60%,#F7A8B8 60% 80%,#55CDFC 80%);
--aside-bg:linear-gradient(180deg,#FFFFFF,#dff6ff 48%,#ffe7ee);
--work-bg:linear-gradient(180deg,#FFFFFF,#dff6ff 52%,#ffe7ee);
--plan-bg:linear-gradient(180deg,#FFFFFF,#ffe7ee);
--code-bg:#FFFFFF;
--button-bg:#FFFFFF; --active-ink:#000000; --focus:#F7A8B8;
--grid-a:rgba(23,37,54,.13); --grid-b:rgba(23,37,54,.09);
--signal:linear-gradient(90deg,#55CDFC,#F7A8B8,#FFFFFF,#F7A8B8,#55CDFC);
--shadow:0 18px 38px rgba(85,205,252,.22);
}
:root[data-theme="dracula"]{
--bg:#282a36; --ink:#f8f8f2; --muted:#b6adc8; --soft:#e6dcff;
--panel:#303241; --panel2:#3b3d4d; --rail:#21222c; --line:#44475a;
--accent:#bd93f9; --amber:#f1fa8c; --red:#ff6e6e; --blue:#8be9fd;
--violet:#ff79c6; --copper:#ffb86c; --ok:#50fa7b; --white:#fff;
--header-bg:rgba(33,34,44,.97); --aside-bg:rgba(33,34,44,.94);
--work-bg:linear-gradient(180deg,#303241,#282a36);
--plan-bg:rgba(48,50,65,.95); --code-bg:#1f2029;
--button-bg:#3b3d4d; --active-ink:#211f2a; --focus:#f1fa8c;
--grid-a:rgba(248,248,242,.07); --grid-b:rgba(189,147,249,.06);
--signal:linear-gradient(90deg,#bd93f9,#ff79c6,#ff5555,#ffb86c,#50fa7b,#8be9fd);
--shadow:0 18px 46px rgba(18,19,25,.38);
}
:root[data-theme="solarized-dark"]{
--bg:#002b36; --ink:#93a1a1; --muted:#93a1a1; --soft:#eee8d5;
--panel:#073642; --panel2:#0b3d49; --rail:#00212a; --line:#586e75;
--accent:#2aa198; --amber:#d2a600; --red:#ff6b61; --blue:#5fb3e8;
--violet:#9aa0ff; --copper:#e27a3f; --ok:#a8b400; --white:#fdf6e3;
--header-bg:rgba(0,43,54,.97); --aside-bg:rgba(0,33,42,.94);
--work-bg:linear-gradient(180deg,#073642,#002b36);
--plan-bg:rgba(7,54,66,.95); --code-bg:#00212a;
--button-bg:#073642; --active-ink:#002b36; --focus:#b58900;
--grid-a:rgba(147,161,161,.09); --grid-b:rgba(42,161,152,.06);
--signal:linear-gradient(90deg,#2aa198,#268bd2,#6c71c4,#b58900,#cb4b16,#dc322f);
--shadow:0 18px 46px rgba(0,24,30,.36);
}
:root[data-theme="solarized-light"]{
--bg:#fdf6e3; --ink:#586e75; --muted:#586e75; --soft:#073642;
--panel:#fffdf4; --panel2:#eee8d5; --rail:#f4efdc; --line:#93a1a1;
--accent:#2aa198; --amber:#a15f00; --red:#dc322f; --blue:#1769aa;
--violet:#5f5fb2; --copper:#7c4a12; --ok:#597000; --white:#002b36;
--header-bg:rgba(253,246,227,.98); --aside-bg:rgba(244,239,220,.96);
--work-bg:linear-gradient(180deg,#fffdf4,#fdf6e3);
--plan-bg:rgba(238,232,213,.96); --code-bg:#eee8d5;
--button-bg:#fffdf4; --active-ink:#002b36; --focus:#6c71c4;
--grid-a:rgba(88,110,117,.1); --grid-b:rgba(42,161,152,.07);
--signal:linear-gradient(90deg,#2aa198,#268bd2,#6c71c4,#b58900,#cb4b16,#dc322f);
--shadow:0 18px 34px rgba(88,110,117,.14);
}
:root[data-theme="twilight"]{
--bg:#141414; --ink:#f8f8f8; --muted:#9b9b9b; --soft:#d7d7d7;
--panel:#1e1e1e; --panel2:#272727; --rail:#111; --line:#3f3a32;
--accent:#cda869; --amber:#f9ee98; --red:#cf6a4c; --blue:#7587a6;
--violet:#9b859d; --copper:#cf7d34; --ok:#8f9d6a; --white:#fff;
--header-bg:rgba(20,20,20,.98); --aside-bg:rgba(17,17,17,.95);
--work-bg:linear-gradient(180deg,#1e1e1e,#141414);
--plan-bg:rgba(30,30,30,.96); --code-bg:#0e0e0e;
--button-bg:#272727; --active-ink:#141414; --focus:#f9ee98;
--grid-a:rgba(248,248,248,.07); --grid-b:rgba(205,168,105,.06);
--signal:linear-gradient(90deg,#cda869,#8f9d6a,#7587a6,#9b859d,#cf6a4c);
--shadow:0 18px 46px rgba(0,0,0,.4);
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.45 "Aptos Mono","IBM Plex Mono",ui-monospace,Menlo,Consolas,monospace}
body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.18;
background-image:linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);
background-image:linear-gradient(var(--grid-a) 1px,transparent 1px),
linear-gradient(90deg,var(--grid-b) 1px,transparent 1px);
background-size:28px 28px}
button{font:inherit;color:inherit;background:var(--panel2);border:1px solid var(--line);
button{font:inherit;color:inherit;background:var(--button-bg);border:1px solid var(--line);
border-radius:6px;padding:8px 10px;cursor:pointer}
button:hover{border-color:var(--accent);color:#fff}
button:hover{border-color:var(--accent);color:var(--white)}
button:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
header{height:58px;display:flex;align-items:center;gap:16px;padding:0 18px;
border-bottom:1px solid var(--line);background:rgba(13,17,23,.96);position:relative;z-index:2}
h1{font-size:15px;margin:0;color:#fff;letter-spacing:.16em;text-transform:uppercase}
border-bottom:1px solid var(--line);background:var(--header-bg);position:relative;z-index:2}
header:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--signal)}
h1{font-size:15px;margin:0;color:var(--white);letter-spacing:.16em;text-transform:uppercase}
.pill{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);
border-radius:999px;padding:4px 9px;color:var(--soft);font-size:12px}
.dot{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 12px var(--red)}
.dot.up{background:var(--ok);box-shadow:0 0 12px var(--ok)}
.spacer{flex:1}
.tls{color:var(--accent)}
.settings-wrap{position:relative}
.settings-menu{position:absolute;right:0;top:calc(100% + 10px);width:min(430px,calc(100vw - 24px));
max-height:calc(100vh - 84px);overflow:auto;z-index:8;background:var(--panel);
border:1px solid var(--line);border-radius:8px;box-shadow:var(--shadow);padding:12px}
.settings-menu[hidden]{display:none}
.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:10px}
.settings-title{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.settings-current{margin-top:4px;color:var(--white)}
.settings-close{padding:4px 8px;line-height:1}
.theme-switch{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.theme-btn{display:grid;grid-template-columns:18px 1fr;align-items:center;gap:8px;min-width:0;
padding:9px;border-color:var(--line);background:var(--panel2);color:var(--soft);text-align:left}
.theme-btn.active{background:var(--panel2);border-color:var(--accent);color:var(--white);
box-shadow:inset 3px 0 0 var(--accent)}
.swatch{width:16px;height:16px;border-radius:4px;border:1px solid var(--line);background:var(--signal)}
.theme-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.theme-btn[data-theme="console"] .swatch{background:linear-gradient(135deg,#0a0c0f 0 45%,#22c7a9 45% 65%,#d8925b 65%)}
.theme-btn[data-theme="paper"] .swatch{background:linear-gradient(135deg,#edf2f7 0 45%,#167b83 45% 65%,#b43449 65%)}
.theme-btn[data-theme="contrast"] .swatch{background:linear-gradient(135deg,#000 0 45%,#00e5ff 45% 65%,#ffd84d 65%)}
.theme-btn[data-theme="pride"] .swatch{background:linear-gradient(135deg,#e40303 0 17%,#ff8c00 17% 34%,#ffed00 34% 51%,#008026 51% 68%,#24408e 68% 84%,#732982 84%)}
.theme-btn[data-theme="trans"] .swatch{background:linear-gradient(135deg,#55CDFC 0 20%,#F7A8B8 20% 40%,#FFFFFF 40% 60%,#F7A8B8 60% 80%,#55CDFC 80%)}
.theme-btn[data-theme="dracula"] .swatch{background:linear-gradient(135deg,#282a36 0 40%,#bd93f9 40% 58%,#ff79c6 58% 74%,#50fa7b 74%)}
.theme-btn[data-theme="solarized-dark"] .swatch{background:linear-gradient(135deg,#002b36 0 44%,#2aa198 44% 62%,#b58900 62% 78%,#dc322f 78%)}
.theme-btn[data-theme="solarized-light"] .swatch{background:linear-gradient(135deg,#fdf6e3 0 44%,#2aa198 44% 62%,#b58900 62% 78%,#dc322f 78%)}
.theme-btn[data-theme="twilight"] .swatch{background:linear-gradient(135deg,#141414 0 44%,#cda869 44% 62%,#8f9d6a 62% 78%,#cf6a4c 78%)}
.layout{display:grid;grid-template-columns:330px minmax(420px,1fr) 390px;
height:calc(100vh - 58px);position:relative;z-index:1}
aside,.work,.plan{min-height:0;overflow:auto}
aside{background:rgba(13,17,23,.9);border-right:1px solid var(--line)}
.work{background:linear-gradient(180deg,#0f1319,#0b0d11);padding:18px}
.plan{background:rgba(17,21,27,.92);border-left:1px solid var(--line);padding:16px}
aside{background:var(--aside-bg);border-right:1px solid var(--line)}
.work{background:var(--work-bg);padding:18px}
.plan{background:var(--plan-bg);border-left:1px solid var(--line);padding:16px}
.section-title{display:flex;align-items:center;justify-content:space-between;
padding:13px 14px 8px;color:var(--muted);font-size:11px;letter-spacing:.12em;
text-transform:uppercase}
.metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:16px}
.metric{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px}
.metric b{display:block;font-size:24px;line-height:1;color:#fff}
.metric b{display:block;font-size:24px;line-height:1;color:var(--white)}
.metric span{display:block;color:var(--muted);font-size:11px;margin-top:8px;text-transform:uppercase}
.metric.crit b{color:var(--red)} .metric.high b{color:var(--amber)} .metric.med b{color:var(--blue)}
.metric.posture b{color:var(--accent)}
@ -53,7 +198,7 @@
.item:hover{background:var(--panel)}
.item.sel{background:var(--panel2);border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
.item .top{display:flex;align-items:center;gap:8px;justify-content:space-between}
.id{font-size:12px;color:#fff;word-break:break-word}
.id{font-size:12px;color:var(--white);word-break:break-word}
.time,.fine{color:var(--muted);font-size:11px}
.tags{margin-top:7px;display:flex;gap:6px;flex-wrap:wrap}
.tag{font-size:11px;border:1px solid var(--line);border-radius:999px;padding:2px 7px;color:var(--soft)}
@ -63,40 +208,40 @@
.sev.MEDIUM{color:var(--blue);border-color:var(--blue)}
.tabs{display:flex;gap:6px;margin-bottom:14px}
.tabs button{padding:7px 11px}
.tabs button.active{background:var(--accent);border-color:var(--accent);color:#00110e}
.card{background:rgba(17,21,27,.9);border:1px solid var(--line);border-radius:8px;
.tabs button.active{background:var(--accent);border-color:var(--accent);color:var(--active-ink)}
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;
padding:14px;margin-bottom:14px;box-shadow:var(--shadow)}
h2{font-size:18px;margin:0 0 8px;color:#fff;letter-spacing:0}
h2{font-size:18px;margin:0 0 8px;color:var(--white);letter-spacing:0}
h3{font-size:12px;margin:0 0 10px;color:var(--muted);letter-spacing:.12em;text-transform:uppercase}
.kv{display:grid;grid-template-columns:120px 1fr;gap:8px;margin-top:10px}
.kv div:nth-child(odd){color:var(--muted)}
.timeline{display:grid;gap:8px}
.event{border-left:2px solid var(--accent);padding:8px 10px;background:rgba(255,255,255,.03)}
.event b{display:block;color:#fff}
.event code,.cmd{display:block;margin-top:6px;padding:8px;background:#080a0d;
border:1px solid var(--line);border-radius:6px;color:#cbd5e1;white-space:pre-wrap;word-break:break-word}
.event{border-left:2px solid var(--accent);padding:8px 10px;background:var(--panel2)}
.event b{display:block;color:var(--white)}
.event code,.cmd{display:block;margin-top:6px;padding:8px;background:var(--code-bg);
border:1px solid var(--line);border-radius:6px;color:var(--ink);white-space:pre-wrap;word-break:break-word}
.rule-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:12px}
.rule-counts{display:flex;gap:7px;flex-wrap:wrap}
.rule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}
.rule-card{position:relative;overflow:hidden;border:1px solid var(--line);
border-radius:8px;background:linear-gradient(135deg,rgba(17,21,27,.98),rgba(10,12,15,.96));
border-radius:8px;background:var(--panel);
padding:12px 12px 12px 16px}
.rule-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--accent)}
.rule-card.syscall:before{background:var(--violet)}
.rule-card.exec:before{background:var(--copper)}
.rule-meta{display:flex;align-items:center;gap:8px;justify-content:space-between;margin-bottom:8px}
.rule-sid{font-size:12px;color:#fff}
.rule-sid{font-size:12px;color:var(--white)}
.rule-msg{color:var(--ink);margin:8px 0 10px}
.cond{display:grid;grid-template-columns:98px 1fr;gap:6px;border-top:1px solid rgba(255,255,255,.06);padding-top:7px}
.cond{display:grid;grid-template-columns:98px 1fr;gap:6px;border-top:1px solid var(--line);padding-top:7px}
.cond dt{color:var(--muted);font-size:11px}
.cond dd{margin:0;color:var(--soft);font-size:11px;word-break:break-word}
.action{border:1px solid var(--line);border-radius:8px;background:#0d1117;padding:11px;margin-bottom:10px}
.action{border:1px solid var(--line);border-radius:8px;background:var(--panel);padding:11px;margin-bottom:10px}
.action .head{display:flex;gap:8px;align-items:center;justify-content:space-between}
.risk{font-size:10px;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:2px 7px;color:var(--muted)}
.risk.high{color:var(--red);border-color:var(--red)}
.risk.medium{color:var(--amber);border-color:var(--amber)}
.risk.low{color:var(--accent);border-color:var(--accent)}
pre{margin:0;white-space:pre-wrap;word-break:break-word;color:#c8d1df;font-size:12px}
pre{margin:0;white-space:pre-wrap;word-break:break-word;color:var(--ink);font-size:12px}
.empty,.err{padding:30px;color:var(--muted);text-align:center}
.err{color:var(--red)}
@media (max-width:1100px){
@ -105,6 +250,8 @@
}
@media (max-width:760px){
header{height:auto;min-height:58px;flex-wrap:wrap;padding:12px}
.settings-wrap{order:2}
.settings-menu{position:fixed;left:12px;right:12px;top:72px;width:auto}
.layout{display:block;height:auto}
aside,.work,.plan{height:auto;max-height:none}
.metrics{grid-template-columns:repeat(2,1fr)}
@ -119,6 +266,20 @@
<span class="pill" id="host">host unknown</span>
<span class="spacer"></span>
<span class="pill" id="ebpf">eBPF unknown</span>
<div class="settings-wrap">
<button id="settingsBtn" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="settingsMenu">Settings</button>
<div id="settingsMenu" class="settings-menu" hidden>
<div class="settings-head">
<div>
<div class="settings-title">Settings</div>
<div class="settings-current" id="themeCurrent">Theme: Console</div>
</div>
<button class="settings-close" id="settingsClose" type="button" title="Close settings">Close</button>
</div>
<div class="section-title"><span>Theme</span><span>local</span></div>
<div class="theme-switch" id="themeOptions" role="group" aria-label="Theme"></div>
</div>
</div>
<button id="refreshBtn" title="Refresh console">Refresh</button>
</header>
@ -156,6 +317,14 @@
const TOKEN = new URLSearchParams(location.search).get("token") || "";
const HDR = TOKEN ? {Authorization:"Bearer "+TOKEN} : {};
const TABS = new Set(["incident","alerts","posture","rules","events"]);
const THEME_OPTIONS = [
["console","Console"],["paper","Paper"],["contrast","Contrast"],
["pride","LGBTQ"],["trans","Trans"],["dracula","Dracula"],
["solarized-dark","Solarized Dark"],["solarized-light","Solarized Light"],
["twilight","Twilight"]
];
const THEMES = new Set(THEME_OPTIONS.map(t=>t[0]));
const THEME_LABELS = Object.fromEntries(THEME_OPTIONS);
function tabFromHash(){
const name = location.hash.replace(/^#/,"");
return TABS.has(name) ? name : "incident";
@ -173,6 +342,42 @@ function el(tag, cls, txt){const e=document.createElement(tag); if(cls)e.classNa
function sev(s){return el("span","sev "+(s||""),s||"?")}
function tags(xs){const box=el("div","tags"); (xs||[]).forEach(x=>box.append(el("span","tag",x))); return box}
function fmt(t){return (t||"?").replace("T"," ").slice(0,19)}
function themeName(){
const current = document.documentElement.dataset.theme || "console";
return THEMES.has(current) ? current : "console";
}
function setTheme(name){
const theme = THEMES.has(name) ? name : "console";
document.documentElement.dataset.theme = theme;
try{ localStorage.setItem("sentinel.theme", theme); }catch(_e){}
const current = document.getElementById("themeCurrent");
if(current) current.textContent = "Theme: " + THEME_LABELS[theme];
document.querySelectorAll(".theme-btn").forEach(b=>{
const active = b.dataset.theme === theme;
b.classList.toggle("active", active);
b.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function renderThemeButtons(){
const box = document.getElementById("themeOptions");
if(!box) return;
box.innerHTML = "";
THEME_OPTIONS.forEach(([name,label])=>{
const b = el("button","theme-btn");
b.type = "button"; b.dataset.theme = name; b.title = label + " theme";
b.append(el("span","swatch"));
b.append(el("span","theme-name",label));
b.onclick = ()=>setTheme(name);
box.append(b);
});
}
function setSettings(open){
const menu = document.getElementById("settingsMenu");
const btn = document.getElementById("settingsBtn");
if(!menu || !btn) return;
menu.hidden = !open;
btn.setAttribute("aria-expanded", open ? "true" : "false");
}
async function refresh(){
try{
@ -388,8 +593,21 @@ function showError(msg){
document.getElementById("plan").innerHTML = '<div class="err">'+msg+'</div>';
}
document.querySelectorAll(".tabs button").forEach(b=>b.onclick=()=>{state.tab=b.dataset.tab; location.hash=state.tab; renderTab();});
document.getElementById("settingsBtn").onclick=e=>{
e.stopPropagation();
const menu = document.getElementById("settingsMenu");
setSettings(menu ? menu.hidden : true);
};
document.getElementById("settingsClose").onclick=()=>setSettings(false);
document.getElementById("settingsMenu").onclick=e=>e.stopPropagation();
document.addEventListener("click",()=>setSettings(false));
document.addEventListener("keydown",e=>{
if(e.key === "Escape") setSettings(false);
});
window.addEventListener("hashchange",()=>{state.tab=tabFromHash(); renderTab();});
document.getElementById("refreshBtn").onclick=refresh;
renderThemeButtons();
setTheme(themeName());
refresh();
setInterval(refresh, 10000);
</script>