feat(assets): item pictures on the marketplace and the character sheet (Phase 5) #38

Merged
whitlocktech merged 1 commits from feat/asset-bridge-p5 into edge 2026-09-11 11:27:05 +00:00
Member

Phase 5 of the Asset Bridge, website half (docs/link/v8.md §5, §11, §11.1, §12). Pairs with servuo-plugins#32 and docs#240. link needed nothing — it forwards verbatim in both directions.

Both places this site already knew an item's (ItemID, hue) and could only print it as text now show the picture. The shard does the hueing: whether a hue repaints every pixel or only the grey ones is a flag in tiledata.mul, which a browser has no way to read.

Ingest warms; the route only serves

Org lead, 2026-09-11. A page never waits on the shard and never causes a fetch — it renders what is stored and leaves out what is not, which is the state every install was in before this phase and which every surface already handles. Fetching happens behind that, on a timer, from the keys the site's own rows name.

Fetching on first request was rejected on one number: the shard's asset plane serves one request at a time by design (§3.2), so a URL that fetched would let any anonymous visitor walk 49,152 ids × 3,000 hues through that single slot and park an operator's own import behind it. Warming from the site's own data has no such surface — the ceiling is the number of distinct (item, hue) pairs the shard has already told us about.

The wanted set is derived, not queued

SELECT DISTINCT item_id, hue FROM shard_vendor_items. Self-healing by construction: a restart loses nothing, and a key stops being wanted the moment the vendor row naming it is deleted. A queue table would need writing on the ingest path, draining, retrying, pruning and reconciling — for the same answer.

The bounded in-memory hint set on top is only for the character sheet, which is fetched live from the shard per request and stored nowhere. Nothing on disk would ever name those keys, so noticing them as they go past is the only way a pass can find them.

Staleness without a manifest (§7)

Every row records the shard's catalog id — a hash of the files that decide its bytes. A client patch changes it; a restart does not. So "is this picture out of date?" is a per-row column comparison, and pictures nobody looks at any more are simply never re-fetched, which is why this is lazy rather than a sweep.

The alternative — a static manifest family enumerating 39,189 rows with hashes — would cost a 34-second scan of the whole art file per Update to answer a question about maybe three hundred pictures.

Three things worth not rediscovering

  • shard_asset_meta is never written by a warm pass. It is the body catalogue's singleton — what an Update compares a manifest against — and a warm pass touching it would tell the body import that a client it never looked at is unchanged. The creature catalogue would quietly stop updating. Tested.
  • A key the shard has no art for writes no row at all. An empty row would make it "held" and it would never be asked again — including after the operator patches in the graphic that was missing. Tested.
  • h0 is not a key. Hue 0 on the wire means "not hued", so staticKey produces the plain key. The shard refuses /h0 for the same reason; the two agreeing is what stops one PNG being stored twice under two names and diffed separately forever. Tested.

An older overlay is a reported state, not a warning loop

assets.sources now reports which families an overlay serves. A phase-3 or phase-4 plugin serves the creature catalogue and nothing else, and every static key sent to it comes back refused — once per pass, forever, with no picture ever appearing. Checked once instead, with code: "UNSUPPORTED" and a sentence naming the fix.

Added

  • POST /admin/shard/assets/warm — run a pass now instead of waiting for the timer; {force} recovers a wiped uploads volume, {limit} bounds it.
  • GET /admin/shard/assets now reports loaded.items / loaded.land and shard.families.
  • shard_assets.catalog column; pictures under uploads/items/, content-addressed like the creature portraits.
  • art on market listings and on character-sheet equipment — a filename, never a URL, the same contract shard_spawn_creatures.art uses.
  • ItemIcon on the marketplace, the vendor page and the sheet, where it replaces the empty swatch the sheet has always drawn.

Checks

  • 688 server tests pass (14 new), client builds
  • swagger fragment regenerated
  • frozen manifest regenerated against the pinned core: one route added, all documented, no core URL moved

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Phase 5 of the Asset Bridge, website half (`docs/link/v8.md` §5, §11, §11.1, §12). Pairs with servuo-plugins#32 and docs#240. `link` needed nothing — it forwards verbatim in both directions. Both places this site already knew an item's `(ItemID, hue)` and could only print it as text now show the picture. The shard does the hueing: whether a hue repaints every pixel or only the grey ones is a flag in `tiledata.mul`, which a browser has no way to read. ## Ingest warms; the route only serves Org lead, 2026-09-11. A page never waits on the shard and never causes a fetch — it renders what is stored and leaves out what is not, which is the state every install was in before this phase and which every surface already handles. Fetching happens behind that, on a timer, from the keys the site's own rows name. Fetching on first request was rejected on one number: the shard's asset plane serves **one request at a time** by design (§3.2), so a URL that fetched would let any anonymous visitor walk 49,152 ids × 3,000 hues through that single slot and park an operator's own import behind it. Warming from the site's own data has no such surface — the ceiling is the number of distinct `(item, hue)` pairs the shard has already told us about. ## The wanted set is derived, not queued `SELECT DISTINCT item_id, hue FROM shard_vendor_items`. Self-healing by construction: a restart loses nothing, and a key stops being wanted the moment the vendor row naming it is deleted. A queue table would need writing on the ingest path, draining, retrying, pruning and reconciling — for the same answer. The bounded in-memory hint set on top is only for the **character sheet**, which is fetched live from the shard per request and stored nowhere. Nothing on disk would ever name those keys, so noticing them as they go past is the only way a pass can find them. ## Staleness without a manifest (§7) Every row records the shard's `catalog` id — a hash of the files that decide its bytes. A client patch changes it; a restart does not. So "is this picture out of date?" is a per-row column comparison, and pictures nobody looks at any more are simply never re-fetched, which is why this is lazy rather than a sweep. The alternative — a `static` manifest family enumerating 39,189 rows with hashes — would cost a 34-second scan of the whole art file per Update to answer a question about maybe three hundred pictures. ## Three things worth not rediscovering - **`shard_asset_meta` is never written by a warm pass.** It is the body catalogue's singleton — what an Update compares a manifest against — and a warm pass touching it would tell the body import that a client it never looked at is unchanged. The creature catalogue would quietly stop updating. Tested. - **A key the shard has no art for writes no row at all.** An empty row would make it "held" and it would never be asked again — including after the operator patches in the graphic that was missing. Tested. - **`h0` is not a key.** Hue 0 on the wire means "not hued", so `staticKey` produces the plain key. The shard refuses `/h0` for the same reason; the two agreeing is what stops one PNG being stored twice under two names and diffed separately forever. Tested. ## An older overlay is a reported state, not a warning loop `assets.sources` now reports which families an overlay serves. A phase-3 or phase-4 plugin serves the creature catalogue and nothing else, and every static key sent to it comes back refused — once per pass, forever, with no picture ever appearing. Checked once instead, with `code: "UNSUPPORTED"` and a sentence naming the fix. ## Added - `POST /admin/shard/assets/warm` — run a pass now instead of waiting for the timer; `{force}` recovers a wiped uploads volume, `{limit}` bounds it. - `GET /admin/shard/assets` now reports `loaded.items` / `loaded.land` and `shard.families`. - `shard_assets.catalog` column; pictures under `uploads/items/`, content-addressed like the creature portraits. - `art` on market listings and on character-sheet equipment — a **filename**, never a URL, the same contract `shard_spawn_creatures.art` uses. - `ItemIcon` on the marketplace, the vendor page and the sheet, where it replaces the empty swatch the sheet has always drawn. ## Checks - **688 server tests pass** (14 new), client builds - swagger fragment regenerated - frozen manifest regenerated against the pinned core: **one route added, all documented, no core URL moved** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-11 11:17:34 +00:00
feat(assets): item pictures on the marketplace and the character sheet (Phase 5)
All checks were successful
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / frozen-manifest (pull_request) Successful in 41s
PR Checks / server-tests (pull_request) Successful in 8m23s
f335531538
Both places this site already knew an item's (ItemID, hue) and could only print
it as text now show the picture, hued the way the client would draw it. The
shard does the hueing: whether a hue repaints every pixel or only the grey ones
is a flag in `tiledata.mul`, which a browser has no way to read.

**Ingest warms; the route only serves** (org lead, 2026-09-11). A page never
waits on the shard and never causes a fetch -- it renders what is stored and
leaves out what is not, which is the state every install was in before this
phase. Fetching happens behind that, on a timer, from the keys the site's own
rows name. The alternative, fetching on first request, was rejected on one
number: the shard's asset plane serves ONE request at a time, so a URL that
fetched would let any visitor walk 49,152 ids times 3,000 hues through that slot
and park an operator's own import behind it.

The wanted set is DERIVED (`SELECT DISTINCT item_id, hue`) rather than queued, so
it is self-healing: a restart loses nothing, and a key stops being wanted the
moment the vendor row naming it is deleted. The in-memory hint set on top is only
for the character sheet, which is fetched live from the shard and stored nowhere
-- nothing on disk would ever name those keys.

Staleness without a manifest (§7): every row records the shard's `catalog` id, a
hash of the files that decide its bytes. A client patch changes it and a restart
does not, so "is this out of date?" is a per-row question -- and pictures nobody
looks at any more are simply never re-fetched, which is why this is lazy rather
than a sweep. `shard_asset_meta` is deliberately NOT written here: it is the body
catalogue's singleton, and a warm pass touching it would tell the body import
that a client it never looked at is unchanged.

A key the shard has no art for writes no row at all. An empty row would make the
key held and it would never be asked again -- including after the operator
patches in the graphic that was missing.

`assets.sources` now reports which families an overlay serves, so an overlay
older than phase 5 is one reported state with a sentence naming the fix, instead
of a refusal per pass forever with no picture ever appearing.

688 server tests pass (14 new); client builds; the frozen manifest regenerates
with one added route, all documented, no core URL moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 53b3aca0c1 into edge 2026-09-11 11:27:05 +00:00
whitlocktech deleted branch feat/asset-bridge-p5 2026-09-11 11:27:06 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#38
No description provided.