feat(assets): item pictures on the marketplace and the character sheet (Phase 5)
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
This commit is contained in:
@@ -204,6 +204,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/shard/assets/warm": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Admin · Shard"
|
||||
],
|
||||
"summary": "Fetch item and land artwork the site is missing, now (admin only)",
|
||||
"description": "Runs one pass of the item-art warm loop instead of waiting for its timer. The pass works out which item pictures this site",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "What the pass did",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UoItemArtWarmResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Admin role required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
},
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Re-fetch pictures this site already holds."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "How many keys this pass may fetch (1-2000)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/shard/atlas": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -8477,6 +8539,40 @@
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Item pictures held. Unlike the catalogue this has no total to compare against: item art is fetched because something on the site names it, so this is the working set rather than a fraction of one."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 1840
|
||||
}
|
||||
}
|
||||
},
|
||||
"land": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Land tile pictures held. Zero on every install until something asks for one."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8625,6 +8721,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"families": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "array"
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Which asset key families this shard’s plugin overlay serves. An overlay older than phase 5 answers `[\"body\"]` only — it has the creature catalogue and no item art."
|
||||
},
|
||||
"example": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
"body",
|
||||
"land",
|
||||
"static"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9321,6 +9450,293 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UoItemArtWarmResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "object"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Outcome of one item-art warm pass (docs/link/v8.md §11, phase 5). Unlike the body catalogue there is no manifest and no set: the client addresses 49,152 item graphics times three thousand hues, so what gets fetched is defined by what this site’s own rows name — every distinct (ItemID, hue) on a player vendor, plus anything a character sheet has shown since the last pass. Reported rather than thrown, so a shard that is down is an answer and not a 500."
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
"skipped",
|
||||
"unavailable",
|
||||
"unchanged",
|
||||
"imported",
|
||||
"failed"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "`skipped`: no shard is configured. `unchanged`: every wanted picture is already held and current. `unavailable`: the shard could not be asked, or its plugin overlay is too old to serve item art."
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "imported"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Machine-readable cause. `NO_IMAGING` is a shard host with no libgdiplus. `UNSUPPORTED` is a plugin overlay that serves the creature catalogue but not item art — update the overlay."
|
||||
},
|
||||
"enum": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
"DISABLED",
|
||||
"NO_SOURCE",
|
||||
"SHARD_DOWN",
|
||||
"PROTOCOL",
|
||||
"BUSY",
|
||||
"NO_IMAGING",
|
||||
"UNSUPPORTED",
|
||||
"INCOMPLETE",
|
||||
"STUCK",
|
||||
"MALFORMED",
|
||||
"TOO_LARGE",
|
||||
"UNAVAILABLE"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "The shard’s art catalogue id these pictures were fetched under — a hash of the files that decide their bytes. Stored per row, which is how staleness is answered without a manifest."
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "7c1e04b9aa2f3d58"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wanted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Distinct keys this site’s rows name right now."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 1840
|
||||
}
|
||||
}
|
||||
},
|
||||
"held": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "How many of those are already stored and current."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 1440
|
||||
}
|
||||
}
|
||||
},
|
||||
"asked": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "How many this pass actually requested. Bounded by `limit`."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 400
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetched": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "How many the shard returned a picture for."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 396
|
||||
}
|
||||
}
|
||||
},
|
||||
"written": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "How many were written to disk."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 396
|
||||
}
|
||||
}
|
||||
},
|
||||
"absent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Keys the shard has no art for. NOT a failure — 9,963 of this client’s static ids have an empty index entry, and an item using one simply has no picture."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"unsupported": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Keys the shard does not serve at all. A bug on the site’s side rather than a gap in the client."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"remaining": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "integer"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Wanted keys left for the next pass. Passes repeat on a timer, so a backlog drains without an operator."
|
||||
},
|
||||
"example": {
|
||||
"type": "number",
|
||||
"example": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UoShardLinkRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user