name: adom-screensaver-billboard description: > How the Adom Screensaver's Wiki billboard pipeline works: where it pulls content, how it caches, how often it re-checks, how it minimizes hits to wiki.adom.inc, how it minimizes downloads to the user's machine, and how it scores which drop to show. Read this before touching the Wiki loader, the cache, the scoring, or the refresh cadence in adom-screensaver.cs / billboard.html. Triggers: screensaver caching, billboard refresh, hero cache, manifest, delta skip, scoring, freshness, why is the screensaver showing an old hero, minimize wiki hits, screensaver bandwidth.

Adom Screensaver - billboard caching, scoring & refresh

The screensaver plays Adom Wiki pages as billboards. This documents the data pipeline so it stays honest about server load and bandwidth. Source of truth: Wiki class in adom-screensaver.cs + billboard.html. Everything below is verified behavior, not aspiration.

One source only

It pulls from exactly one wiki: VW = https://wiki.adom.inc (the canonical Adom Wiki). The old V1 / V2 constants are dead (no code path queries them). All requests are authenticated with the user's bearer token (wiki-token.txt), so the billboards are the user's filtered view (public + their private/org pages, each badged).

When it refreshes

Wiki.Start() → spawns Wiki.Run() once per screensaver launch (every idle-fire, or Win+Shift+L). There is no in-session wiki re-poll. billboard.html re-reads the local manifest.json every 3 s (never touches the wiki). Last run's billboards are shown instantly from the cached manifest while the fresh Run() reconciles in the background.

In-session in-place refresh (the one-launch-lag fix). loadManifest doesn't just append new slugs - it also refreshes a slide already on screen when its media/updated changes (repainting the current card if it's the one showing). Without this, the page kept showing the CACHED hero from launch even after the fresh Run() downloaded an updated one into the manifest, so a hero you updated would only appear on the NEXT launch. Now it appears within ~3 s of the background fetch, same session, no reordering.

Where heroes live now (wiki change, 2026-06). Hero images are no longer committed repo files - the page's /files listing returns NO images. The hero is in a hero store, exposed via the page DETAIL's hero_path (e.g. screenshots/hero.png) + hero_type, and served at the usual /blob/<type>/<slug>/<hero_path>. So discovery MUST read hero_path from the detail (the LIST omits it - it comes back empty there) and the RepoFilesAll fallback is now mostly dead weight (kept for older pages). The canonical /blob/.../screenshots/hero.png URL still works, so updated heroes are fetched correctly (verified: the cached -hid hero is byte-identical to live).

Minimizing wiki-server load - the DELTA SKIP (the important part)

Per launch, Run() issues just 2 list calls: GET /api/v1/pages?type=app&limit=100 and …type=skill…. Those list items already carry each page's updated_at.

Then for each page, ProcessItem does a delta skip: if last run's manifest already has that slug at the same updated_at and its cached hero file is still on disk, it reuses that entry verbatim

  • no per-page detail fetch, no file-list, no hero probe, no download. Only new or changed pages do the full network work (detail + file-list + ranged-GET hero probes + maybe a hero download).

Net: a re-fire where nothing changed costs 2 requests total (verified: WIKI: run done, 60 reused (delta-skip), 0 fetched, 60 total). Before this, every launch re-fetched all ~60 pages' detail + files + probes - hundreds of requests per idle-fire.

The cache (what's on disk)

%LOCALAPPDATA%\adom-screensaver\cache\:

  • manifest.json - the slide list (slug, title, brief, prompt, media ref, vis, owner, updated, …). Rewritten each run from the fresh+reused set; a page removed from the wiki simply drops out.
  • Hero images (image/SVG) - downloaded and served locally, filename keyed on the page's update stamp: SafeName(slug)__<compacted updated_at>.png. So:
    • page unchanged → same filename → served from disk, no re-download;
    • page's hero updated → new updated_at → new filename → re-downloaded once, and the old stamped file (+ the pre-stamp legacy slug.png) is pruned.
    • The download URL is cache-busted (?v=<stamp>) so no proxy serves stale bytes.
  • showcounts.json - per-slug lifetime view counts (host-written synchronously on each show), used by the scorer. Survives hard kills (unlike browser localStorage).

Minimizing downloads / bandwidth

  • Heroes are downloaded at most once per content change (the stamp key above). Unchanged heroes are never re-fetched.
  • Videos are STREAMED, never downloaded (they can be tens of MB); the manifest carries the remote URL and the WebView2 streams it.
  • The delta skip means unchanged pages download nothing.

Repo type (App / Skill / Component) - capsule + components opt-in

The wiki's type field is first-class (app / skill / component / bootstrap) - it's what the header tabs filter on, NOT tags. The fetch reads it per page and the manifest carries s.type. Each billboard shows a color-coded type capsule leading its meta row: App = blue (square dot), Skill = purple (diamond), Component = pink (round). IsHeroType decides what's eligible: app + skill always; component only when opted in; bootstrap never.

Components are opt-in (Prefs.ShowComponents(), default OFF; config toggle compToggle -> components:on/components:off). When ON, AddWork also fetches type=component and Start() widens Target 200 -> 360 so components survive the per-launch collection cap (app+skill alone already exceed 200). Takes effect on the next saver launch.

Component heroes - raster product visuals only, never a symbol

Most type=component pages are jellybean passive parts (0402 caps/resistors) whose only image is a part-number-named svg, i.e. a schematic symbol, and they rarely set hero_path. Showing those full-screen is a joke. So RepoFilesAll is component-strict:

  • Skip ALL svg for components (svg == symbol / schematic / pcb diagram / part-number symbol). Real product visuals (3D renders, photos) are raster (png/jpg). Also skip any schematic/symbol/footprint file for every type.
  • Prefer a clean 3D hero angle: hero files first, then 3D renders ranked home/iso/persp (clean product shots) > plain front/board (often a viewer screenshot or a dimensioned view) > edge/underside back/bottom/left/right/top/side, then screenshots, then pcb layout last.
  • Components show ONLY with a genuine hero / 3D render / photo: the assembled candidate list for a component is hero + 3D + screenshot only (NO "rest" random-raster or bare-PCB fallback). A component with no real product visual gets no hero and is dropped - better no card than a junk card.
  • Net (verified on ALL 200 live components): 38 billboard, 162 dropped, 0 junk - and all 38 have a real visual (32 resolve to a clean 3d-home.png render, the rest to *-hero.png / product-photo.jpg). Reference: cosmocoil -> 3d-home.png.

No network (offline) - never dead-stop, never burn in

The saver must survive a launch with no wifi (e.g. a fresh Airbnb) without stranding a bright static splash on the panel. Three guarantees, none of which depend on the wiki being reachable:

  1. Don't clobber the cache. AddWork returns whether the list call got a real HTTP response. If the wiki was unreachable, Run() skips WriteManifest() so last run's good manifest.json stays on disk, and re-publishes the cached entries into Count. (Previously Run() always wrote the manifest from an empty published list, wiping last run's billboards - the hero files stayed on disk but the manifest that indexed them was zeroed, so the page had 0 slides and froze on the splash.)
  2. Play cached drops offline. The page reads the preserved manifest.json via the virtual host and rotates the cached billboards normally. The splash says Offline: playing your N saved drops from last time. (or Offline: no saved drops yet; dimming to protect your screen. on a true first run).
  3. Burn-in backstops the network can't defeat (host OnStatusTick): dim early (45s) when the run finished/failed with nothing to show (Wiki.Done && Wiki.Count==0), and an absolute 8-min ceiling (HardRestCapMs) so even RestAfter=0 ("never") can't hold a lit screen all night. The normal RestAfterSec timer (default 300s) still applies on top.

Scoring - what plays first (billboard.html shuffleSlides)

Higher score plays sooner; the goal is to lead with NEW + UNSEEN, sink OLD + over-seen:

  • Freshness (from updated): <7d +100, <30d +60, <90d +35, <365d +15, older/unknown +5.
  • Novelty (from showcounts.json): max(0, 60 - 20 × timesShown) (never-seen +60; gone after ~3 views).
  • Jitter: +0..12 so the order varies run-to-run and ties never lock. Each billboard prints its own priority N (fresh F + new V) in the footer for transparency.

Public vs private background (so the two never get confused)

Each billboard's full background is tinted by visibility (set per slide in showSlide via a viz-pub/viz-priv body class driving #bgwash): PUBLIC = the clean dark screensaver (no wash); PRIVATE / org-only = an Adom TEAL wash + a teal screen frame (brand #00B8B1). This makes a private page (e.g. adom-gchat-adom, adom-google-adom, or any -dev source page) unmistakable next to its public twin.

Private pages only appear if the saver is authenticated. The list/detail/blob calls all send the wiki-token.txt bearer. The hero-probe (UrlExists) MUST send it too: a private page's hero blob 401/404s anonymously, so without the token every private page was silently dropped (the bug that made private -adom/-dev pages never show). Public heroes probe fine anonymously, which masked it.

Freshness display

Each slide shows a chip from updated: "New today" / "Updated 3 days ago" / "Updated 2 weeks ago" …, teal for recent, dim for old.

Exclude list

exclude.txt (beside the .scr) holds slugs kept OUT of the rotation, one per line. Skip() also hard-excludes adom-screensaver, test-/smoke-/shared- prefixes, adom-desktop/hd-, and auth pages. On startup FilterCachedManifestByExclude() drops now-excluded slugs from the cached manifest so a just-excluded card never flashes.

Correctness invariants (don't regress)

  • One entry per slug. AddWork dedups by slug within the work list; each launch is a fresh process so published never accumulates across runs. (If you ever see two of the same slug, that's the bug.) Note: two different slugs about the same topic (e.g. adom-gchat the CLI + adom-gchat-adom the Org Config page) are two real pages, not a duplicate.
  • A reused (delta-skip) entry is only trusted if its hero file still exists (CachedMediaOk); else it falls through to full processing.
  • The manifest is rebuilt from the current wiki list each run, so removed pages drop and the set is never stale.

Known gaps → asks filed for Colby (see colby-feedback.md, 2026-06-19)

  • /blob/... sends no-cache with no ETag/Last-Modified and ignores conditional GET, so the only way to know a hero changed without a page edit is to download it. Want ETag + 304.
  • last_commit_hash is only in the page detail, not the list - with it in the list we'd have a perfect per-page/per-hero delta key in one call.
  • No bulk popularity/trending endpoint (stars are per-page only) - want star_count in the list and/or /api/v1/trending to factor popularity into the score.

Quick reference

Question Answer
Which wiki? wiki.adom.inc only
Re-check cadence once per saver launch (no in-session poll)
Requests when nothing changed 2 (the two list calls); 0 per-page
Hero re-download only when updated_at changes
Videos streamed, never downloaded
Order freshness + novelty + jitter (highest first)