app
Adom Screensaver
Public Made by Adomby adom
Vibe-coding screensaver, fades up a live billboard of the latest Adom Wiki drops (truly cached to disk; plays webm silently, renders SVG) with a power-aware nerd-stats splash, while signed native keep
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
<!doctype html>
<!-- Adom Screensaver Settings. Rendered windowed in a WebView2 by adom-screensaver.scr /c
(and, later, by the Hydrogen Desktop dialog). The C# host injects LIVE Windows settings via
window.adomConfig(data, applied) every time it opens, and applies the recommended profile when
the user clicks Apply (the host re-reads + re-injects so the table always shows real state). -->
<html lang="en">
<head>
<meta charset="utf-8">
<style>
:root { --teal:#00b8b0; --tealbri:#00e6dc; --hi:#e6edf3; --lo:#8b949e; --dim:#6b7280;
--bg:#0d1117; --panel:#161b22; --line:#21262d; --warn:#e3a008; --ok:#2ea043; }
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; background:var(--bg); color:var(--hi);
font-family:"Segoe UI",system-ui,sans-serif; user-select:none; }
body { display:flex; flex-direction:column; height:100vh; overflow:hidden; }
/* App shell: sticky header + tab bar on top, scrolling panel in the middle, sticky footer at the bottom. */
.topbar { flex:none; padding:22px 30px 0; }
.tabs { display:flex; gap:2px; margin-top:18px; padding:0 30px; border-bottom:1px solid var(--line); }
.tab { font-family:inherit; font-size:13px; font-weight:600; color:var(--lo); background:none;
border:none; border-radius:0; padding:11px 17px; cursor:pointer; border-bottom:2px solid transparent;
margin-bottom:-1px; letter-spacing:.01em; }
.tab:hover { color:var(--hi); border-bottom-color:transparent; }
.tab.active { color:var(--tealbri); border-bottom-color:var(--teal); }
.tab .tdot { display:none; width:7px; height:7px; border-radius:50%; background:var(--warn);
margin-left:7px; vertical-align:1px; box-shadow:0 0 6px rgba(227,160,8,.7); }
.content { flex:1; overflow-y:auto; padding:6px 30px 26px; }
.panel { display:none; }
.panel.active { display:block; }
.panel .tune:first-child, .panel .why:first-child, .panel table:first-child { margin-top:14px; }
.footbar { flex:none; display:flex; align-items:center; gap:12px; padding:13px 30px;
border-top:1px solid var(--line); background:var(--panel); }
.head { display:flex; align-items:center; gap:14px; }
.head img { height:30px; }
.head .t { font-size:17px; font-weight:600; letter-spacing:.02em; }
.head .t small { display:block; color:var(--teal); font-size:11px; letter-spacing:.42em;
text-transform:uppercase; font-weight:600; margin-top:2px; }
.why { margin-top:18px; background:var(--panel); border:1px solid var(--line);
border-left:3px solid var(--teal); border-radius:8px; padding:15px 18px; }
.why h2 { font-size:13.5px; color:var(--tealbri); font-weight:600; margin-bottom:8px; }
.why p { font-size:12.5px; line-height:1.55; color:var(--lo); margin-bottom:8px; }
.why p:last-child { margin-bottom:0; }
.why b { color:var(--hi); font-weight:600; }
.why .code { color:var(--tealbri); font-weight:600; }
.status { margin-top:16px; display:flex; flex-wrap:wrap; gap:10px 22px; font-size:12.5px;
padding:0 2px; }
.status .k { color:var(--dim); }
.status .v { color:var(--hi); font-weight:600; }
.status .note { color:var(--lo); font-size:11.5px; }
table { margin-top:14px; width:100%; border-collapse:collapse; }
th { text-align:left; font-size:10px; letter-spacing:.16em; text-transform:uppercase;
color:var(--dim); font-weight:600; padding:0 8px 7px; }
th.r, td.r { text-align:right; }
td { padding:9px 8px; border-top:1px solid var(--line); vertical-align:top; font-size:12.5px; }
td .lab { color:var(--hi); font-weight:600; }
td .why2 { color:var(--lo); font-size:11px; line-height:1.35; margin-top:2px; max-width:330px; }
td.cur { color:var(--hi); white-space:nowrap; }
td.rec { color:var(--lo); white-space:nowrap; }
tr.bad td.cur { color:var(--warn); font-weight:600; }
.badge { display:inline-block; min-width:20px; text-align:center; font-size:11px; font-weight:700; }
.badge.ok { color:var(--ok); }
.badge.fix { color:var(--warn); }
.hkrow { margin-top:16px; display:flex; align-items:center; justify-content:space-between; gap:18px;
padding:13px 15px; border:1px solid var(--line); border-radius:8px; background:var(--panel); }
.hklab { font-size:12.5px; color:var(--hi); font-weight:600; }
.hklab b { color:var(--tealbri); }
.hkwhy { font-size:11px; color:var(--lo); line-height:1.35; margin-top:3px; max-width:440px; }
.switch { position:relative; display:inline-block; width:42px; height:24px; flex:none; cursor:pointer; }
.switch input { opacity:0; width:0; height:0; }
.track { position:absolute; inset:0; background:#2a3340; border-radius:999px; transition:.2s; }
.track:before { content:""; position:absolute; height:18px; width:18px; left:3px; top:3px; background:var(--lo); border-radius:50%; transition:.2s; }
.switch input:checked + .track { background:var(--teal); }
.switch input:checked + .track:before { transform:translateX(18px); background:#04211f; }
.banner { margin-top:16px; border-radius:8px; padding:11px 15px; font-size:12.5px; font-weight:600; }
.banner.ok { background:rgba(46,160,67,.12); border:1px solid rgba(46,160,67,.4); color:#5dd879; }
.banner.bad { background:rgba(227,160,8,.12); border:1px solid rgba(227,160,8,.4); color:#f0c548; }
.actions { margin-top:18px; display:flex; align-items:center; gap:12px; }
button { font-family:inherit; font-size:13px; font-weight:600; border-radius:8px; cursor:pointer;
padding:10px 18px; border:1px solid var(--line); background:var(--panel); color:var(--hi); }
button:hover { border-color:var(--teal); }
button.primary { background:var(--teal); border-color:var(--teal); color:#04211f; }
button.primary:hover { background:var(--tealbri); }
button.primary[disabled] { background:#16332f; border-color:#16332f; color:#4b7a76; cursor:default; }
.spacer { flex:1; }
.foot { margin-top:14px; color:var(--dim); font-size:11px; line-height:1.5; }
.toast { color:var(--ok); font-size:12.5px; font-weight:600; opacity:0; transition:opacity .3s; }
.toast.show { opacity:1; }
.spin { display:inline-block; width:11px; height:11px; margin-right:7px; vertical-align:-1px;
border:2px solid rgba(255,255,255,.22); border-top-color:var(--teal); border-radius:50%;
animation:spin .6s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
button:disabled { opacity:.8; cursor:default; }
/* Fine-tune section (#2 rest-after, #3 idle timeout, #4 never-off) */
select { font-family:inherit; font-size:12.5px; font-weight:600; color:var(--hi); background:#1b2330;
border:1px solid #2c3645; border-radius:7px; padding:7px 10px; cursor:pointer; flex:none; }
select:hover { border-color:var(--teal); }
.tune { margin-top:16px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:0 15px; }
.tune .trow { display:flex; align-items:center; justify-content:space-between; gap:18px;
padding:13px 0; border-top:1px solid var(--line); }
.tune .trow:first-child { border-top:none; }
.tune .tlab { font-size:12.5px; color:var(--hi); font-weight:600; }
.tune .tlab b { color:var(--tealbri); }
.tune .twhy { font-size:11px; color:var(--lo); line-height:1.35; margin-top:3px; max-width:430px; }
.tune .twhy.warn { color:var(--warn); }
.casc-intro { font-size:11.5px; color:var(--lo); line-height:1.5; padding:13px 0 4px; }
.casc-intro b { color:var(--hi); }
.tune .tlab .step { display:inline-block; width:17px; height:17px; line-height:17px; text-align:center;
border-radius:50%; background:var(--teal); color:#04211f; font-size:10.5px; font-weight:700;
margin-right:7px; vertical-align:1px; }
.tune .tlab .sub { color:var(--lo); font-weight:400; }
/* Display-dimming hardware test */
.tune .hw { font-size:11.5px; color:var(--lo); line-height:1.45; padding:13px 0 10px; }
.tune .hw b { color:var(--hi); }
.btestbtn { font-family:inherit; font-size:12px; font-weight:600; color:var(--tealbri); background:#0f141b;
border:1px solid var(--line); border-radius:7px; padding:7px 13px; cursor:pointer; margin:0 0 12px; }
.btestbtn:hover { border-color:var(--teal); }
.prow { display:flex; align-items:center; gap:13px; padding:0 0 12px; }
.prow .btestbtn { margin:0; flex:none; min-width:215px; }
.prow .pwhy { font-size:11px; color:var(--lo); line-height:1.4; }
.prow .pwhy b { color:var(--hi); }
.btestbox { display:none; align-items:center; gap:14px; padding:0 0 12px; }
.btestbox .bpct { font-size:30px; font-weight:700; color:var(--tealbri); font-variant-numeric:tabular-nums; min-width:90px; }
.btestbox .bnote { font-size:11.5px; color:var(--lo); }
.bresult { font-size:11.5px; line-height:1.4; padding:0 0 13px; }
.bresult.ok { color:#5dd879; } .bresult.no { color:var(--warn); } .bresult.un { color:var(--lo); }
/* Modern Standby explainer + live state */
.msbox { margin-top:16px; border-radius:8px; padding:13px 15px; border:1px solid var(--line); background:var(--panel); }
.msbox.ok { border-left:3px solid #2ea043; }
.msbox.warn { border-left:3px solid var(--warn); }
.msbox .mshdr { font-size:12.5px; font-weight:700; color:var(--hi); margin-bottom:6px; }
.msbox .mswhy { font-size:11.5px; color:var(--lo); line-height:1.5; margin-bottom:9px; }
.msbox .mswhy b { color:var(--hi); }
.msbox .msstate { font-size:11.5px; line-height:1.5; }
.msbox .msstate.ok { color:#5dd879; } .msbox .msstate.warn { color:#f0c548; }
.msbox .msstate b { font-weight:700; }
/* Cache / debug tab: every page the saver has fetched, the hero it stored, and the score it computes */
.cachebar { display:flex; align-items:center; gap:14px; margin-top:14px; flex-wrap:wrap; font-size:12px; }
.cachebar .cnt { color:var(--lo); } .cachebar .cnt b { color:var(--hi); }
.cachebar select { padding:5px 8px; font-size:11.5px; }
.cachebar .reload { font-family:inherit; font-size:11.5px; font-weight:600; color:var(--tealbri); background:#0f141b;
border:1px solid var(--line); border-radius:7px; padding:6px 11px; cursor:pointer; }
.cachebar .reload:hover { border-color:var(--teal); }
#cacheList { margin-top:8px; }
.ccard { display:flex; gap:13px; padding:13px 2px; border-top:1px solid var(--line); }
.ccard:first-child { border-top:none; }
.cthumbwrap { flex:none; width:108px; }
.cthumb { width:108px; height:68px; border-radius:6px; border:1px solid var(--line); object-fit:cover; background:#0a0e14; display:block; }
.cthumb.miss { display:flex; align-items:center; justify-content:center; color:var(--warn); font-size:10px; font-weight:600; text-align:center; }
.cmeta { flex:1; min-width:0; }
.cmeta .cslug { color:var(--tealbri); font-weight:700; font-size:13px; word-break:break-all; }
.cmeta .ctitle { color:var(--hi); font-weight:600; font-size:12px; margin-top:2px; }
.cmeta .cbrief { color:var(--lo); font-size:11px; line-height:1.4; margin-top:3px; }
.cmeta .cfile { color:var(--dim); font-size:10px; margin-top:5px; word-break:break-all; line-height:1.4; }
.cmeta .cfile .none { color:var(--warn); }
.ctype { display:inline-block; font-size:9px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
padding:1px 7px; border-radius:999px; vertical-align:1.5px; margin-left:7px; }
.ctype.t-app{background:rgba(56,184,255,.16);color:#7cc6ff;} .ctype.t-skill{background:rgba(140,107,247,.2);color:#b9a3ff;}
.ctype.t-component{background:rgba(244,114,182,.16);color:#f7a8d8;} .ctype.t-bootstrap{background:rgba(46,160,67,.16);color:#5dd879;}
.cscore { flex:none; width:118px; text-align:right; }
.cscore .big { font-size:21px; font-weight:700; color:var(--tealbri); font-variant-numeric:tabular-nums; line-height:1; }
.cscore .brk { font-size:10px; color:var(--lo); margin-top:5px; line-height:1.5; }
.cscore .brk .f { color:#7cc6ff; font-weight:600; } .cscore .brk .n { color:#5dd879; font-weight:600; }
.cscore .brk .j { color:var(--dim); }
.cdlbtn { font-family:inherit; font-size:10.5px; font-weight:600; color:var(--tealbri); background:#0f141b;
border:1px solid var(--line); border-radius:6px; padding:6px 11px; cursor:pointer; margin-top:8px; white-space:nowrap; }
.cdlbtn:hover { border-color:var(--teal); } .cdlbtn:disabled { opacity:.45; cursor:default; }
.ccmp { margin-top:10px; display:none; gap:13px; flex-wrap:wrap; }
.ccmp.show { display:flex; }
.ccmp .side { flex:1 1 0; min-width:0; }
.ccmp .side .lbl { font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); margin-bottom:5px; }
.ccmp .side img { width:100%; border-radius:6px; border:1px solid var(--line); background:#0a0e14; display:block; }
.ccmp .side .dim { font-size:10px; color:var(--lo); margin-top:4px; font-variant-numeric:tabular-nums; }
.ccmp .side .err { font-size:10px; color:var(--warn); margin-top:4px; line-height:1.4; }
.ccmp .verdict { flex:0 0 100%; width:100%; margin-top:3px; font-size:11px; line-height:1.4; }
.ccmp .verdict.same { color:#5dd879; } .ccmp .verdict.diff { color:var(--warn); }
</style>
</head>
<body>
<div class="topbar">
<div class="head">
<img src="adom-wordmark.png" alt="Adom">
<div class="t">Screensaver<small>Settings</small></div>
</div>
<div class="tabs" id="tabs">
<button class="tab active" data-tab="cascade">Cascade</button>
<button class="tab" data-tab="power">Power<span class="tdot" id="powerDot"></span></button>
<button class="tab" data-tab="preview">Preview</button>
<button class="tab" data-tab="cache">Cache</button>
<button class="tab" data-tab="about">About</button>
</div>
</div>
<div class="content">
<!-- ============ TAB: CASCADE (what plays & when) ============ -->
<div class="panel active" id="tab-cascade">
<div class="tune" id="cascade">
<div class="casc-intro">The screensaver runs as a timed <b>cascade</b>: the Adom logo, then the splash, then the Wiki billboards, then it dims. Set each stage:</div>
<div class="trow">
<div>
<div class="tlab"><span class="step">1</span> Starts after <span class="sub">(idle)</span></div>
<div class="twhy">How long the machine sits idle before the screensaver appears.</div>
</div>
<select id="saverSel">
<option value="60">1 min</option><option value="120">2 min</option><option value="180">3 min</option>
<option value="300">5 min</option><option value="600">10 min</option><option value="900">15 min</option>
</select>
</div>
<div class="trow">
<div>
<div class="tlab"><span class="step">2</span> Adom logo holds</div>
<div class="twhy">The Adom mark shows at least this long before the splash (it may run a touch longer while the billboard page finishes loading).</div>
</div>
<select id="logoSel">
<option value="0">Skip</option><option value="1">1 sec</option><option value="2">2 sec</option>
<option value="3">3 sec</option><option value="5">5 sec</option><option value="8">8 sec</option>
</select>
</div>
<div class="trow">
<div>
<div class="tlab"><span class="step">3</span> Splash holds</div>
<div class="twhy">The Adom splash screen stays up this long before the billboards begin rotating.</div>
</div>
<select id="splashSel">
<option value="2">2 sec</option><option value="3">3 sec</option><option value="5">5 sec</option>
<option value="8">8 sec</option><option value="10">10 sec</option><option value="15">15 sec</option><option value="30">30 sec</option>
</select>
</div>
<div class="trow">
<div>
<div class="tlab"><span class="step">4</span> Each billboard shows for</div>
<div class="twhy">How long each Wiki billboard stays on screen before the next one fades in.</div>
</div>
<select id="slideSel">
<option value="3">3 sec</option><option value="4">4 sec</option><option value="5">5 sec</option>
<option value="8">8 sec</option><option value="10">10 sec</option><option value="15">15 sec</option><option value="30">30 sec</option><option value="60">60 sec</option>
</select>
</div>
<div class="trow">
<div>
<div class="tlab"><span class="step">5</span> Dim the screen after</div>
<div class="twhy" id="restWhy">After the screensaver starts, the billboards play for this long, then it rests: plugged in it dims the <b>backlight to near-black</b> (the laptop stays <b>awake</b> so your /remote-control sessions keep running), on battery it powers the panel off to save battery. The dim plus slow drift are what prevent burn-in.</div>
</div>
<select id="restSel">
<option value="60">1 min</option><option value="120">2 min</option><option value="180">3 min</option>
<option value="300">5 min</option><option value="600">10 min</option><option value="900">15 min</option>
<option value="0">Never</option>
</select>
</div>
</div>
<div class="tune" id="content" style="margin-top:16px">
<div class="trow" style="border-top:none">
<div>
<div class="tlab">Include component billboards</div>
<div class="twhy">By default the billboards show <b>apps</b> and <b>skills</b>. Turn this on to also rotate in <b>components</b> (eval boards, modules, parts) from the Wiki. Every billboard is labeled with its kind. Takes effect the next time the screensaver runs.</div>
</div>
<label class="switch"><input type="checkbox" id="compToggle"><span class="track"></span></label>
</div>
</div>
</div><!-- /tab-cascade -->
<!-- ============ TAB: POWER (keep the machine awake & reachable) ============ -->
<div class="panel" id="tab-power">
<div class="msbox warn" id="msbox" style="display:none">
<div class="mshdr">Modern Standby, and why this screensaver exists</div>
<div class="mswhy">On a <b>Modern Standby</b> laptop, the moment the display turns <b>off</b>, Windows treats that as a cue to drop into <b>light sleep</b>: it <b>cuts your network and throttles the CPU</b>, so any <b>/remote-control</b> session quietly dies. That's the whole reason this app exists. Instead of turning the display off, it keeps the screen <b>on and dims it to near-black</b>, so your laptop stays fully awake and your sessions survive, while the dim plus slow drift still prevent burn-in.</div>
<div class="msstate" id="msstate"></div>
</div>
<div class="status" id="status"></div>
<table>
<thead><tr>
<th>Setting</th><th class="r">Your setting</th><th class="r">Adom recommends</th><th class="r"></th>
</tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="banner" id="banner"></div>
<div class="actions">
<button class="primary" id="apply">Apply Adom Recommended</button>
<button id="recheck">Re-check</button>
<span class="toast" id="toast">Applied ✓</span>
</div>
<div class="tune" style="margin-top:16px">
<div class="trow" style="border-top:none">
<div>
<div class="tlab">Never let Windows turn the display off</div>
<div class="twhy" id="neverWhy">Recommended <b>ON</b> while plugged in. Keeps the display on so the laptop stays <b>awake</b> and your <b>/remote-control</b> sessions survive; the screensaver dims the backlight to near-black and drifts to prevent burn-in. Letting Windows power the display off makes the laptop doze and drops your sessions.</div>
</div>
<label class="switch"><input type="checkbox" id="neverToggle"><span class="track"></span></label>
</div>
</div>
<div class="tune" id="bsection" style="margin-top:16px">
<div class="hw" id="hwinfo">Checking your display hardware…</div>
<button class="btestbtn" id="btest">Test if the screensaver can dim my screen</button>
<div class="btestbox" id="btestbox"><span class="bpct" id="bpct">0%</span><span class="bnote" id="bnote"></span></div>
<div class="bresult un" id="bresult">Not tested yet.</div>
</div>
<div class="hkrow">
<div>
<div class="hklab">Global hotkey, launch with <b>Win + Shift + L</b></div>
<div class="hkwhy">Press it anytime to show the screensaver on demand. Turn off if it collides with another shortcut you use.</div>
</div>
<label class="switch"><input type="checkbox" id="hkToggle"><span class="track"></span></label>
</div>
</div><!-- /tab-power -->
<!-- ============ TAB: PREVIEW (see it live) ============ -->
<div class="panel" id="tab-preview">
<div class="tune" id="previewsection">
<div class="hw">See it for real on a live Wiki billboard. <b>Move the mouse to exit</b> the preview and come back here.</div>
<div class="prow">
<button class="btestbtn" id="previewapp">Test an app billboard</button>
<span class="pwhy">Shows a real <b>app</b> page with its blue <b>App</b> capsule, at normal brightness.</span>
</div>
<div class="prow">
<button class="btestbtn" id="previewskill">Test a skill billboard</button>
<span class="pwhy">Shows a real <b>skill</b> page with its purple <b>Skill</b> capsule.</span>
</div>
<div class="prow">
<button class="btestbtn" id="previewcomp">Test a component billboard</button>
<span class="pwhy">Shows a real <b>component</b> page (pink <b>Component</b> capsule), eval boards, modules and parts, fetched on demand even if component billboards are turned off.</span>
</div>
<div class="prow">
<button class="btestbtn" id="previewpriv">Test a private billboard</button>
<span class="pwhy">Shows a real <b>private / org-only</b> billboard so you can see the <b>teal background + frame</b> that marks internal pages (vs the plain public look).</span>
</div>
<div class="prow">
<button class="btestbtn" id="previewdim">See the dimmed screen</button>
<span class="pwhy" id="previewdimwhy">Jumps straight to the dimmed screensaver: the Adom logo and <b>AI Electronics Prototyping</b> drifting slowly, near-black (billboards don't rotate when dimmed). See how dark it really gets.</span>
</div>
</div>
</div><!-- /tab-preview -->
<!-- ============ TAB: CACHE (what's stored on disk + the score I compute) ============ -->
<div class="panel" id="tab-cache">
<div class="hw" style="font-size:11.5px;color:var(--lo);line-height:1.5;padding:13px 0 2px">
A live look at the saver's on-disk billboard cache: every Wiki page it has fetched, the <b>hero image it
stored</b>, the page's title & description, and the <b>priority score</b> it computes to order the
rotation (higher = shown sooner). Hit <b>Download current from Wiki</b> on any row to pull that page's
hero <b>right now</b> and compare it side-by-side with what's cached, so you can spot a stale hero.
</div>
<div class="cachebar">
<span class="cnt" id="cacheCnt">Reading cache…</span>
<span style="color:var(--dim)">sort</span>
<select id="cacheSort"><option value="score">Score</option><option value="slug">Slug</option><option value="fresh">Freshness</option><option value="type">Type</option></select>
<button class="reload" id="cacheReload">↻ Reload from disk</button>
</div>
<div id="cacheList"></div>
</div><!-- /tab-cache -->
<!-- ============ TAB: ABOUT (why this exists) ============ -->
<div class="panel" id="tab-about">
<div class="why">
<h2 id="whyHead">Why your machine needs different rules in the AI age</h2>
<div id="whyBody"></div>
</div>
<div class="foot">Reads your <b>live</b> Windows settings every time it opens, never a saved copy.
Change anything in Control Panel and re-open (or hit Re-check) to see it here. Most settings apply
without admin; the display still turns off on schedule to protect against burn-in.</div>
</div><!-- /tab-about -->
</div><!-- /content -->
<div class="footbar">
<span class="spacer"></span>
<button id="close">Close</button>
</div>
<script>
// Host-agnostic: in the .scr it's WebView2 (chrome.webview); embedded in the Hydrogen Desktop
// dialog it's an iframe, so fall back to posting up to the parent window.
function send(m){
try{ if(window.chrome && window.chrome.webview){ window.chrome.webview.postMessage(m); return; } }catch(e){}
try{ window.parent && window.parent.postMessage({ adomScreensaver: m }, "*"); }catch(e){}
}
var recheckAt = 0; // timestamp of an in-flight Re-check, so adomConfig can finish the spinner
// HD pushes config data via postMessage instead of ExecuteScriptAsync. Accept either shape.
window.addEventListener("message", function(ev){
var d = ev && ev.data; if(!d) return;
if (d.adomConfigData) { try{ window.adomConfig(d.adomConfigData, !!d.applied); }catch(e){} }
});
function esc(s){ return (s==null?"":String(s)).replace(/&/g,"&").replace(/</g,"<"); }
var CAP = {}; // last hardware capabilities (device/dozeRisk/brightCtl), stashed by adomConfig
// The intro paragraph is hardware-shaped: a laptop gets the lid/battery/bag story; a desktop gets the
// simpler "keep the box awake + show the billboard" story; and whether display-off is dangerous depends
// on Modern Standby, not the chassis.
function renderIntro(cap){
cap = cap || {};
var head = document.getElementById("whyHead"), body = document.getElementById("whyBody");
if (!head || !body) return;
var desktop = cap.device === "desktop";
var doze = !!cap.dozeRisk;
head.textContent = desktop ? "Why your PC needs different rules in the AI age"
: "Why your laptop needs different rules in the AI age";
var dev = desktop ? "PC" : "laptop";
var p = [];
p.push("Windows ships set to <b>aggressively sleep and power down</b> by default, choices Microsoft made long "
+ "before AI agents ran for hours on your behalf. The moment the machine idles "
+ (desktop ? "" : "or the lid shuts ") + "it sleeps, and anything still working dies with it.");
p.push("But when you're working with <b>Claude</b> and <b>Adom Hydrogen Desktop</b>, you want the AI to "
+ "<b>keep chugging</b>: long sessions that finish, <span class=\"code\">/remote-control</span> threads that "
+ "don't drop while you step away, and your " + dev + " quietly becoming a <b>vibe-coding server</b> that "
+ "drives your frontend, takes screenshots, and builds <b>shotlogs you can watch from your phone</b>. None of "
+ "that happens on a sleeping machine.");
if (doze){
p.push("This " + dev + " uses <b>Modern Standby</b>, so the moment the display turns <b>off</b>, Windows drops "
+ "into <b>light sleep</b> that cuts your network and throttles the CPU, killing those sessions. So this app, "
+ "from your <b>system tray</b> (the Adom mark by your clock), keeps the machine <b>awake</b>, shows this "
+ "<b>Wiki billboard</b>, then <b>dims the screen to near-black</b> instead of turning the display off, "
+ "the dim plus slow drift prevent burn-in while the machine stays awake and your sessions survive.");
} else {
p.push("This " + dev + " does <b>not</b> use Modern Standby, so turning the display off won't drop your network. "
+ "What still matters is not letting the machine <b>sleep</b>, that's what kills /remote-control here. So this "
+ "app, from your <b>system tray</b> (the Adom mark by your clock), keeps the machine <b>awake</b> and shows "
+ "this <b>Wiki billboard</b>"
+ (cap.brightCtl === 1
? ", dimming the backlight to near-black when you step away so the billboard never burns in."
: ". Since it can't dim this display's backlight, it lets the monitor power off on a timer (safe here, and easier on the panel).") );
}
p.push("And if another app (a <b>screen-share, recording, or media tab</b>) is holding your display on so the "
+ "screensaver can't start, the tray icon turns into a <b style=\"color:var(--warn)\">⚠</b> and names what's "
+ "blocking it. Right-click that icon any time for these settings. Here's what we recommend for <b>your</b> "
+ "hardware, and what you've got right now:");
body.innerHTML = "<p>" + p.join("</p><p>") + "</p>";
}
// Modern Standby state + guidance. On hardware that doesn't support MS (most desktops) the whole
// doze danger is moot, so we say so plainly instead of nagging about a feature that can't apply.
window.adomMS = function(ms, cap){
ms = ms || {}; cap = cap || CAP || {};
var box = document.getElementById("msbox"), st = document.getElementById("msstate");
var hdr = box ? box.querySelector(".mshdr") : null, why = box ? box.querySelector(".mswhy") : null;
if (!box || !st) return;
box.style.display = "block";
var cls, m;
if (ms.supported === false){
// No Modern Standby on this machine: display-off is safe, only sleep is the enemy.
cls = "ok";
if (hdr) hdr.textContent = "Modern Standby: not on this machine (that's fine)";
if (why) why.innerHTML = "<b>Modern Standby</b> is the laptop low-power mode that, on machines that have it, "
+ "treats a display-off as a cue to drop into light sleep, cutting the network and throttling the CPU and "
+ "killing <b>/remote-control</b>. <b>This machine doesn't use it</b>, so turning the display off is safe here.";
m = "✓ No doze risk on this hardware. The screensaver just keeps the machine from <b>sleeping</b> (so your "
+ "sessions survive) and shows the Wiki billboard; the display can power off normally without dropping anything.";
} else {
if (hdr) hdr.textContent = "Modern Standby, and why this screensaver exists";
if (why) why.innerHTML = "On a <b>Modern Standby</b> machine, the moment the display turns <b>off</b>, Windows "
+ "treats that as a cue to drop into <b>light sleep</b>: it <b>cuts your network and throttles the CPU</b>, so "
+ "any <b>/remote-control</b> session quietly dies. That's the whole reason this app exists. Instead of turning "
+ "the display off, it keeps the screen <b>on and dims it to near-black</b>, so the machine stays fully awake "
+ "and your sessions survive, while the dim plus slow drift still prevent burn-in.";
if (ms.live && !ms.pending){ cls="ok"; m="✓ Modern Standby is <b>ON</b>, exactly what we want. The lid resumes instantly and the screensaver dims the screen (no slow hibernate)."; }
else if (!ms.live && ms.pending && ms.after){ cls="warn"; m="⚠ You're <b>between two states</b>: Modern Standby is <b>OFF right now</b> but set to turn back <b>ON after a restart</b>. <b>Restart</b> to finish, then you get instant wake plus the dim-not-off behavior."; }
else if (ms.live && ms.pending && !ms.after){ cls="warn"; m="⚠ Modern Standby is <b>ON now</b> but set to turn <b>OFF on the next restart</b>, which brings back the slow hibernate. We recommend keeping it on; ask Claude to clear that override."; }
else if (!ms.live && !ms.pending){ cls="warn"; m="⚠ Modern Standby is <b>OFF</b>. For the best experience (instant wake + the dim-not-off screensaver), turn it back on. Ask Claude to re-enable it (needs admin + a restart)."; }
else { cls="ok"; m="Modern Standby is <b>ON</b>."; }
}
box.className = "msbox " + cls;
st.className = "msstate " + cls;
st.innerHTML = m;
};
// Display-dimming hardware probe: the host tells us the panel + whether brightness is controllable, and
// streams the live % during the test as it fades the backlight down then back up.
window.adomHardware = function(h){
h = h || {};
var doze = !!CAP.dozeRisk;
var el = document.getElementById("hwinfo");
if (el) el.innerHTML = "Your display: <b>" + esc(h.monitor || "built-in display") + "</b>. " +
(h.supported ? "Windows exposes a brightness control for this panel, so the screensaver can dim the real <b>backlight</b> to go nearly black when you step away."
: "This hardware does <b>not</b> expose a brightness control to apps, so the screensaver can't dim the real backlight.");
var r = document.getElementById("bresult");
if (!r) return;
// When the backlight isn't controllable, the right fallback depends on doze risk: if Modern Standby
// would kill sessions we MUST keep the display on (dim the picture, a grey, and you can lower brightness
// by hand); if there's no doze, the honest answer is to just let the monitor power off.
if (h.controllable === true){ r.className = "bresult ok"; r.textContent = "✓ Tested: the screensaver CAN control your backlight. It'll go nearly black when you step away."; }
else if (h.controllable === false){
r.className = "bresult no";
r.innerHTML = doze
? "✗ Tested: your backlight isn't controllable, so the saver dims the <b>picture</b> instead (a darker grey, not true black). Because this machine uses Modern Standby we still keep the display ON; for a darker screen, lower brightness by hand before you step away."
: "✗ Tested: your backlight isn't controllable. No Modern Standby here though, so the saver just lets the <b>monitor power off</b> on a timer, that's safe and easier on the panel than a bright billboard all night.";
}
else { r.className = "bresult un"; r.textContent = "Not tested yet. Run the test below to confirm and record it for the saver."; }
};
window.adomBrightTest = function(phase, pct){
var box = document.getElementById("btestbox"); if(box) box.style.display = "flex";
var p = document.getElementById("bpct"); if(p) p.textContent = pct + "%";
var n = document.getElementById("bnote"); if(n) n.textContent =
phase==="down" ? "fading your backlight down…" :
phase==="up" ? "fading back up…" :
phase==="nosupport" ? "your hardware didn't allow it" : "done";
if (phase==="done" || phase==="nosupport"){ setTimeout(function(){ if(box) box.style.display="none"; }, 1500); }
};
(function(){ var b=document.getElementById("btest"); if(b) b.addEventListener("click", function(){ send("btest"); }); })();
(function(){
var ids = ["previewapp","previewskill","previewcomp","previewpriv","previewdim"];
ids.forEach(function(id){ var b=document.getElementById(id); if(b) b.addEventListener("click", function(){ send(id); }); });
})();
function kaNote(ka, onAc){
if (ka === "ac") return "holding your machine awake, so sessions run as long as you like";
if (ka === "dc") return "Windows' sleep timer is the give-up clock; plug in to run indefinitely";
return "the helper isn't running, click Apply to start it";
}
// The host calls this on open (applied=false) and after Apply (applied=true).
window.adomConfig = function(data, applied){
if (!data) return;
CAP = data.cap || {};
try { renderIntro(CAP); } catch(e){}
try { if (window.adomMS) window.adomMS(data.ms, CAP); } catch(e){}
try { rewordTune(CAP); } catch(e){}
var ka = data.keepAwake || "off";
document.getElementById("status").innerHTML =
'<span><span class="k">Power right now:</span> <span class="v">' + esc(data.power) + '</span></span>' +
'<span><span class="k">Keep-awake:</span> <span class="v">' +
(ka==="ac"?"on (plugged)":ka==="dc"?"passive (battery)":"off") +
'</span> <span class="note">- ' + kaNote(ka, data.onAc) + '</span></span>';
var rows = data.rows || [], html = "", bad = 0;
for (var i=0;i<rows.length;i++){
var r = rows[i]; if(!r.ok) bad++;
html += '<tr class="'+(r.ok?"":"bad")+'">' +
'<td><div class="lab">'+esc(r.label)+'</div><div class="why2">'+esc(r.why)+'</div></td>' +
'<td class="cur r">'+esc(r.cur)+'</td>' +
'<td class="rec r">'+esc(r.rec)+'</td>' +
'<td class="r"><span class="badge '+(r.ok?"ok":"fix")+'">'+(r.ok?"✓":"⚠")+'</span></td>' +
'</tr>';
}
document.getElementById("rows").innerHTML = html;
var banner = document.getElementById("banner");
if (bad === 0){
banner.className = "banner ok";
banner.textContent = "✓ Your " + (CAP.device === "desktop" ? "PC" : "laptop") + " matches the Adom vibe-coding profile.";
} else {
banner.className = "banner bad";
banner.textContent = "⚠ " + bad + (bad===1?" setting differs":" settings differ") +
" from the recommended profile, click Apply to bring them in line.";
}
var apply = document.getElementById("apply");
apply.disabled = (bad === 0);
apply.textContent = (bad === 0) ? "Already applied" : "Apply Adom Recommended";
// Surface a warning dot on the Power tab when settings differ, so it's discoverable from any tab.
var pd = document.getElementById("powerDot"); if (pd) pd.style.display = (bad > 0) ? "inline-block" : "none";
var hk = document.getElementById("hkToggle");
if (hk) hk.checked = (data.hotkeyEnabled !== false);
// Fine-tune controls (#2 rest-after, #3 idle timeout, #4 never-off) reflect LIVE state each open.
if (data.saverSec != null) setSel(document.getElementById("saverSel"), data.saverSec);
if (data.restAfterSec != null) setSel(document.getElementById("restSel"), data.restAfterSec);
if (data.logoSec != null) setSelSec(document.getElementById("logoSel"), data.logoSec);
if (data.splashSec != null) setSelSec(document.getElementById("splashSel"), data.splashSec);
if (data.slideSec != null) setSelSec(document.getElementById("slideSel"), data.slideSec);
// The "never turn the display off" toggle is about the PLUGGED-IN rail only (monNeverAc), not both rails;
// on battery we DO let the display sleep, so the old monNever (AC && DC) read OFF even when AC was correct.
var nv = document.getElementById("neverToggle"); if (nv) nv.checked = !!data.monNeverAc;
var ct = document.getElementById("compToggle"); if (ct) ct.checked = !!data.showComponents;
updateNeverWarn();
if (applied){
var t = document.getElementById("toast"); t.textContent = "Applied ✓"; t.classList.add("show");
setTimeout(function(){ t.classList.remove("show"); }, 2600);
}
// finish a Re-check: drop the spinner and flash a confirmation (keep it visible >=550ms)
if (recheckAt){
var rb = document.getElementById("recheck");
var done = function(){
rb.disabled = false; rb.textContent = "Re-check";
var tt = document.getElementById("toast"); tt.textContent = "✓ Re-checked just now"; tt.classList.add("show");
setTimeout(function(){ tt.classList.remove("show"); }, 1700);
recheckAt = 0;
};
var el = Date.now() - recheckAt;
if (el < 550) setTimeout(done, 550 - el); else done();
}
};
document.getElementById("apply").addEventListener("click", function(){ if(!this.disabled) send("apply"); });
document.getElementById("recheck").addEventListener("click", function(){
var rb = this; if (rb.disabled) return;
rb.disabled = true; rb.innerHTML = '<span class="spin"></span>Re-checking…';
recheckAt = Date.now();
send("refresh");
});
document.getElementById("close").addEventListener("click", function(){ send("close"); });
document.getElementById("hkToggle").addEventListener("change", function(){ send(this.checked ? "hotkey:on" : "hotkey:off"); });
(function(){ var ct=document.getElementById("compToggle"); if(ct) ct.addEventListener("change", function(){ send(this.checked ? "components:on" : "components:off"); }); })();
// Pick the matching <option>; if a live value isn't in the list, add it so the control shows reality.
function setSel(sel, val){
if (!sel) return; val = String(val);
sel.value = val;
if (sel.value !== val){
var o = document.createElement("option");
o.value = val; o.textContent = (val === "0") ? "Never" : Math.round(Number(val)/60) + " min";
sel.appendChild(o); sel.value = val;
}
}
// Reword the two fine-tune explainers for the hardware. "Dim the screen after" and "Never turn the
// display off" mean different things on a desktop, and on a machine where we can't dim the backlight.
function rewordTune(cap){
cap = cap || {};
var desktop = cap.device === "desktop", doze = !!cap.dozeRisk, canDim = cap.brightCtl === 1;
var rest = document.getElementById("restWhy");
if (rest){
rest.innerHTML = "The billboards play for this long, then the screensaver rests: "
+ (canDim
? "it dims the <b>backlight to near-black</b>" + (doze ? " (the machine stays <b>awake</b> so your /remote-control sessions keep running)" : "")
: "it dims the picture to near-black" )
+ (desktop ? "" : ", and on battery it powers the panel off to save battery")
+ ". The dim plus slow drift are what prevent burn-in. A few minutes is good.";
}
var pd = document.getElementById("previewdimwhy");
if (pd){
pd.innerHTML = "Jumps straight to the dimmed screensaver: the Adom logo and <b>AI Electronics Prototyping</b> "
+ "drifting slowly, near-black (billboards don't rotate when dimmed), "
+ (canDim
? "with the <b>hardware backlight</b> dropping to near-black on this panel."
: "using the <b>software</b> picture dim (this panel's backlight isn't controllable, so it'll be a dark grey, not true black).");
}
var nw = document.getElementById("neverWhy");
if (nw){
nw.innerHTML = doze
? "Recommended <b>ON</b>" + (desktop ? "" : " while plugged in") + ". This machine uses <b>Modern Standby</b>, so "
+ "powering the display off makes it doze and drops your <b>/remote-control</b> sessions. Keeping it on (the "
+ "saver dims the backlight and drifts) is what keeps you awake and burn-in-free."
: (canDim
? "Optional here. No Modern Standby, so display-off won't drop sessions, but leaving it <b>ON</b> lets the "
+ "saver dim the backlight and run the billboard with no burn-in. Either is safe."
: "Recommended <b>OFF</b> here. No Modern Standby, so letting the monitor power off is safe and won't drop "
+ "sessions; since the saver can't dim this backlight, a powered-off monitor beats a bright billboard all night.");
}
}
function updateNeverWarn(){
// Warn (orange) only when the toggle is set AGAINST our recommendation: OFF on a doze machine, where
// turning the display off would kill sessions. Everywhere else, neutral.
var nv = document.getElementById("neverToggle"), why = document.getElementById("neverWhy");
var against = CAP.dozeRisk && nv && !nv.checked;
if (why) why.className = "twhy" + (against ? " warn" : "");
}
// Like setSel but labels a missing live value in SECONDS (the cascade stages are second-based).
function setSelSec(sel, val){
if (!sel) return; val = String(val);
sel.value = val;
if (sel.value !== val){
var o = document.createElement("option");
o.value = val; o.textContent = (val === "0") ? "Skip" : val + " sec";
sel.appendChild(o); sel.value = val;
}
}
document.getElementById("saverSel").addEventListener("change", function(){ send("saver:" + this.value); });
document.getElementById("restSel").addEventListener("change", function(){ send("restafter:" + this.value); });
(function(){
var lg=document.getElementById("logoSel"); if(lg) lg.addEventListener("change", function(){ send("logo:" + this.value); });
var sp=document.getElementById("splashSel"); if(sp) sp.addEventListener("change", function(){ send("splash:" + this.value); });
var sl=document.getElementById("slideSel"); if(sl) sl.addEventListener("change", function(){ send("slide:" + this.value); });
})();
document.getElementById("neverToggle").addEventListener("change", function(){
send(this.checked ? "displayoff:never" : "displayoff:rec"); updateNeverWarn();
});
// ================= CACHE / DEBUG tab =================
// The config window's web root IS the saver's cache dir, so manifest.json, showcounts.json and every
// cached hero file are served right here. We read them directly and recompute the SAME priority score
// the billboard uses (kept in sync with billboard.html slideScore), so this is a true window into the cache.
function escA(s){ return esc(s).replace(/"/g,""").replace(/'/g,"'"); }
var cacheLoaded = false, cacheData = [], cacheCounts = {};
function ageDaysC(u){ if(!u) return 1e9; var t=Date.parse(u); if(isNaN(t)) return 1e9; return (Date.now()-t)/86400000; }
// freshness: <7d 100, <30d 60, <90d 35, <365d 15, older/unknown 5 (mirror of billboard freshScore)
function freshScoreC(u){ var d=ageDaysC(u); if(d<7)return 100; if(d<30)return 60; if(d<90)return 35; if(d<365)return 15; return 5; }
// novelty: max(0, 60 - 20*timesShown) (never-seen +60; gone after ~3 views)
function noveltyC(slug){ return Math.max(0, 60 - 20*(cacheCounts[slug]||0)); }
function freshLabelC(u){ var d=ageDaysC(u);
if(d>=1e9) return "undated"; if(d<1) return "today"; if(d<2) return "yesterday"; if(d<7) return Math.round(d)+"d ago";
if(d<60) return Math.max(1,Math.round(d/7))+"w ago"; if(d<730) return Math.max(1,Math.round(d/30))+"mo ago"; return Math.round(d/365)+"y ago"; }
async function loadCache(){
var cnt = document.getElementById("cacheCnt"), list = document.getElementById("cacheList");
if (cnt) cnt.textContent = "Reading cache…";
try { var rc = await fetch("showcounts.json?_=" + Date.now(), {cache:"no-store"}); cacheCounts = (await rc.json()) || {}; } catch(e){ cacheCounts = {}; }
try {
var r = await fetch("manifest.json?_=" + Date.now(), {cache:"no-store"});
var m = await r.json(); cacheData = (m && m.slides) || [];
} catch(e){
cacheData = [];
if (cnt) cnt.innerHTML = '<span style="color:var(--warn)">No cache yet — run the screensaver once to populate it.</span>';
if (list) list.innerHTML = "";
return;
}
renderCache();
}
function renderCache(){
var cnt = document.getElementById("cacheCnt"), list = document.getElementById("cacheList");
var sortBy = (document.getElementById("cacheSort") || {}).value || "score";
var rows = cacheData.map(function(s){ var f=freshScoreC(s.updated), n=noveltyC(s.slug); return {s:s, f:f, n:n, score:f+n}; });
if (sortBy==="score") rows.sort(function(a,b){ return b.score-a.score; });
else if (sortBy==="slug") rows.sort(function(a,b){ return (a.s.slug||"").localeCompare(b.s.slug||""); });
else if (sortBy==="fresh") rows.sort(function(a,b){ return b.f-a.f; });
else if (sortBy==="type") rows.sort(function(a,b){ return (a.s.type||"").localeCompare(b.s.type||"") || (b.score-a.score); });
if (cnt) cnt.innerHTML = "<b>" + rows.length + "</b> page" + (rows.length===1?"":"s") + " cached" + (rows.length?" · sorted by " + sortBy:"");
var html = "";
for (var i=0;i<rows.length;i++){
var R = rows[i], s = R.s, media = s.media||"", src = s.src||"", typ = (s.type||"").toLowerCase();
var remoteOnly = /^https?:/i.test(media); // media is a streamed URL (video / un-cached), not a local file
var thumb = media
? '<img class="cthumb" referrerpolicy="no-referrer" src="' + escA(media) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">'
+ '<div class="cthumb miss" style="display:none">no file</div>'
: '<div class="cthumb miss">no hero</div>';
var fileLine = media
? (remoteOnly ? 'streamed (not cached to disk)' : 'cached file: ' + esc(media))
: '<span class="none">no hero file</span>';
html += '<div class="ccard" data-slug="' + escA(s.slug) + '">'
+ '<div class="cthumbwrap">' + thumb + '</div>'
+ '<div class="cmeta">'
+ '<div class="cslug">' + esc(s.slug) + (typ ? '<span class="ctype t-' + esc(typ) + '">' + esc(typ) + '</span>' : '') + '</div>'
+ '<div class="ctitle">' + esc(s.title || "(no title)") + '</div>'
+ '<div class="cbrief">' + esc(s.brief || "") + '</div>'
+ '<div class="cfile">' + fileLine + ' · updated ' + esc(freshLabelC(s.updated)) + (s.vis && s.vis!=="public" ? ' · <span style="color:#f0c548">' + esc(s.vis) + '</span>' : '') + '</div>'
+ '<button class="cdlbtn"' + (src ? ' data-src="' + escA(src) + '" data-media="' + escA(media) + '"' : ' disabled title="no remote URL stored for this entry"') + '>'
+ (src ? 'Download current from Wiki ↓' : 'no remote URL') + '</button>'
+ '<div class="ccmp"></div>'
+ '</div>'
+ '<div class="cscore"><div class="big">' + R.score + '</div>'
+ '<div class="brk"><span class="f">fresh ' + R.f + '</span> + <span class="n">new ' + R.n + '</span><br><span class="j">+ jitter 0–12</span></div></div>'
+ '</div>';
}
if (list) list.innerHTML = html || '<div class="hw" style="padding:18px 0;color:var(--lo)">Cache is empty.</div>';
var btns = list ? list.querySelectorAll(".cdlbtn") : [];
for (var b=0;b<btns.length;b++) (function(btn){ if(!btn.disabled) btn.addEventListener("click", function(){ doCompare(btn); }); })(btns[b]);
}
// Fetch the page's CURRENT hero from the wiki and show it beside the cached one, so a stale cache is obvious.
function doCompare(btn){
var src = btn.getAttribute("data-src"), media = btn.getAttribute("data-media") || "";
var cmp = btn.parentNode.querySelector(".ccmp"); if(!cmp) return;
btn.disabled = true; btn.textContent = "Fetching…";
var bust = (src.indexOf("?")>=0 ? "&" : "?") + "_=" + Date.now();
cmp.className = "ccmp show";
// no-referrer on BOTH: the wiki side needs it to clear Cloudflare hotlink protection (curl-like, no Referer);
// the cached side is local so it's a harmless no-op there.
cmp.innerHTML =
'<div class="side"><div class="lbl">Cached (on disk)</div>'
+ (media ? '<img referrerpolicy="no-referrer" src="' + escA(media) + '" onload="this.parentNode.querySelector(\'.dim\').textContent=this.naturalWidth+\'×\'+this.naturalHeight; cmpReport(this)" onerror="this.style.display=\'none\';this.parentNode.querySelector(\'.err\').textContent=\'cached file missing on disk\'">'
: '<div class="err">no cached file</div>')
+ '<div class="dim"></div><div class="err"></div></div>'
+ '<div class="side"><div class="lbl">Current on Wiki</div>'
+ '<img referrerpolicy="no-referrer" src="' + escA(src) + bust + '" onload="this.parentNode.querySelector(\'.dim\').textContent=this.naturalWidth+\'×\'+this.naturalHeight; cmpReport(this)" onerror="this.style.display=\'none\';this.parentNode.querySelector(\'.err\').textContent=\'couldn\\\'t fetch — private page or offline\'">'
+ '<div class="dim"></div><div class="err"></div></div>'
+ '<div class="verdict" style="display:none"></div>';
setTimeout(function(){ btn.disabled = false; btn.textContent = "↻ Re-download"; }, 1400);
}
// Once both images in a compare have loaded, note whether the dimensions match (a cheap stale-hint; we
// can't pixel-diff a cross-origin image without tainting the canvas, so dimensions + the eyeball test it is).
window.cmpReport = function(img){
var cmp = img.closest(".ccmp"); if(!cmp) return;
var imgs = cmp.querySelectorAll(".side img"), loaded = [];
for (var i=0;i<imgs.length;i++){ if(imgs[i].naturalWidth>0) loaded.push(imgs[i]); }
if (loaded.length < 2) return;
var a = loaded[0], b = loaded[1];
var v = cmp.querySelector(".verdict"); if(!v) return;
var same = (a.naturalWidth===b.naturalWidth && a.naturalHeight===b.naturalHeight);
v.style.display = "block";
v.className = "verdict " + (same ? "same" : "diff");
v.innerHTML = same
? "✓ Same dimensions — cache looks current. (Eyeball them to be sure.)"
: "⚠ Different dimensions (" + a.naturalWidth + "×" + a.naturalHeight + " cached vs " + b.naturalWidth + "×" + b.naturalHeight + " on Wiki) — the cached hero may be stale; the saver will self-heal it on the next fetch.";
};
(function(){
var rl = document.getElementById("cacheReload"); if(rl) rl.addEventListener("click", function(){ loadCache(); });
var so = document.getElementById("cacheSort"); if(so) so.addEventListener("change", function(){ renderCache(); });
})();
// Headless hook for "/cshot <out> cache <slug>": (re)load the cache, find that row, scroll it to the top and
// fire its live wiki compare — so a background screenshot can show the real side-by-side with no mouse.
window.adomCacheCompare = async function(slug){
cacheLoaded = true;
await loadCache();
var cards = document.querySelectorAll("#cacheList .ccard");
for (var i=0;i<cards.length;i++){
if (cards[i].getAttribute("data-slug") === slug){
try { cards[i].scrollIntoView({block:"start"}); } catch(e){}
var btn = cards[i].querySelector(".cdlbtn");
if (btn && !btn.disabled) doCompare(btn);
return true;
}
}
return false;
};
// Tabs: show one panel at a time, reset scroll on switch. Header + tabs + footer stay put; only the
// middle content scrolls, so the long config is now five short, navigable sections.
(function(){
var tabs = document.querySelectorAll('#tabs .tab');
var content = document.querySelector('.content');
function show(name){
for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
var panels = document.querySelectorAll('.panel');
for (var j=0;j<panels.length;j++) panels[j].classList.toggle('active', panels[j].id==='tab-'+name);
if (content) content.scrollTop = 0;
// Lazy-load the cache the first time the tab is opened (and refresh it on every subsequent open).
if (name==='cache'){ if(!cacheLoaded){ cacheLoaded = true; } loadCache(); }
}
for (var k=0;k<tabs.length;k++) (function(t){ t.addEventListener('click', function(){ show(t.getAttribute('data-tab')); }); })(tabs[k]);
// deep link: "/c <tab>" opens config.html#tab=<name> so the window can start on a specific tab
var hm = /[#&]tab=([a-z]+)/.exec(location.hash); if (hm) show(hm[1]);
})();
</script>
</body>
</html>