app
Adom Screensaver
Public Made by Adomby adom
Turn your laptop into an always-on vibe coding server: branded screensaver + native keep-awake (no PowerToys) + honest, hardware-aware power management.
← Commit history
Release 0.7.7: Cache debug tab + Cloudflare no-referrer hero fix + cache self-upgrade
2 files changed
+193−3
dist/win/billboard.html+6−2@@ -451,15 +451,19 @@ function mediaEl(s){ // re-downloads the missing file, so this is belt-and-suspenders: stream now, repopulate the cache later.) const remote = s.src && /^https?:/i.test(s.src) ? s.src : ""; function fallback(el){ if (remote && el.src !== remote){ el.src = remote; return true; } return false; }+ // wiki.adom.inc/blob has Cloudflare HOTLINK PROTECTION: a cross-origin <img>/<video> request carrying a+ // Referer of this page's origin gets a 403 challenge (curl/.NET with no Referer get 200). So when we stream+ // a hero straight from the wiki (the self-heal path, or any un-cached remote media) we MUST send no Referer,+ // exactly like curl does -- otherwise the fallback silently fails and the card still goes blank. if (s.kind === "video"){- const v = document.createElement("video");+ const v = document.createElement("video"); v.referrerPolicy = "no-referrer"; v.src = src; v.muted = true; v.defaultMuted = true; v.volume = 0; v.loop = true; v.autoplay = true; v.setAttribute("playsinline",""); v.preload = "auto"; v.addEventListener("canplay", ()=>{ try{ v.play(); }catch(e){} }); v.onerror = ()=>{ if (!fallback(v)) dropSlide(s.slug); }; return v; }- const img = document.createElement("img"); img.src = src; // <img> renders svg + raster+ const img = document.createElement("img"); img.referrerPolicy = "no-referrer"; img.src = src; // <img> renders svg + raster img.onerror = ()=>{ if (!fallback(img)){ img.style.display="none"; dropSlide(s.slug); } }; return img; }
dist/win/config.html+187−1@@ -139,6 +139,46 @@ .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>@@ -151,6 +191,7 @@ <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>@@ -302,6 +343,23 @@ </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">@@ -620,8 +678,134 @@ 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 four short, navigable sections.+ // 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');@@ -630,6 +814,8 @@ 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