diff --git a/README.md b/README.md index 0f47d6c..6dded11 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ module that follows. │ module-uo (>>> HERE <<<) │ │ shard status · spawn atlas · marketplace │ │ governors · cliloc · town crier · uo-link│ + │ client files: portraits, item art, names │ └───────────────────────────────────────────┘ │ server half: routers, models, schema fragment │ client half: prebuilt ESM chunk, SPA routes + nav @@ -182,7 +183,7 @@ reaches the container. |---|---|---| | `UOLINK_BASE_URL` | — | Default sidecar base URL for a site with nothing saved yet. The admin panel's stored value wins. | | `UOLINK_WS_URL` | — | Same, for the WebSocket URL. | -| `UOLINK_PROTOCOL` | `3` | Wire protocol this build speaks. Again only a fallback — set it lower only if you deliberately run an older sidecar. | +| `UOLINK_PROTOCOL` | `8` | Wire protocol this build speaks. Again only a fallback — set it lower only if you deliberately run an older sidecar. | | `TOWNCRIER_DURATION_SEC` | `3600` | How long a published news post's in-game town-crier message stays up (≤ `86400`). | **The sidecar's auth token is deliberately not here.** It is entered in Admin → Shard, encrypted at diff --git a/client/src/routes/admin/ClientFiles.jsx b/client/src/routes/admin/ClientFiles.jsx index 10c7edd..0ab8bc9 100644 --- a/client/src/routes/admin/ClientFiles.jsx +++ b/client/src/routes/admin/ClientFiles.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import api from '../../api.js' import { ErrorState, Loading } from '../../core.js' import Row from '../../components/DetailRow.jsx' @@ -245,6 +245,28 @@ export default function ClientFiles() { load() }, [load]) + // One automatic re-read when the shard answered BUSY (§3.2's single slot), + // and exactly one per mount. + // + // BUSY is not a fault and it is not sticky on the shard — it means something + // else held the asset slot for longer than the client's own 425 backoff, and + // the two things that hold it are both ordinary: an import the operator + // started, and the item-art warm pass refilling itself after a client patch. + // The panel does not poll, so without this the operator is left reading a + // refusal about a shard that was free again seconds later, until they think to + // reload. A second read clears the common case; if it is still busy, the + // sentence says to come back, because a page that retried forever would be + // holding the slot it is waiting for. + const retried = useRef(false) + useEffect(() => { + if (retried.current || busy) return + const stillBusy = assets?.code === 'BUSY' || clilocs?.code === 'BUSY' + if (!stillBusy) return + retried.current = true + const t = setTimeout(() => load({ quiet: true }), 4000) + return () => clearTimeout(t) + }, [assets, clilocs, busy, load]) + // Every action shares this: run it, say what it said, then re-read status so // the panel reflects the world rather than what we assumed happened. async function run(section, table, fn) { @@ -347,9 +369,22 @@ export default function ClientFiles() { style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }} className="sans" > - The shard is not answering for client files. + {/* BUSY is the one code here that is not a fault, and saying "the shard + is not answering" about it sends an operator to check a shard that is + working. The slot is held by something ordinary — an import running, + or the warm pass — and it frees itself. */} + + {assets.code === 'BUSY' + ? 'The shard is busy with another client-file request.' + : 'The shard is not answering for client files.'} +
- {assets.reason} + {assets.code === 'BUSY' + ? 'The shard serves one of these at a time, so an import running now — or the' + + ' item-picture pass refilling itself after a client patch — holds it until it is' + + ' done. This page re-reads once on its own; if the counts below are still missing' + + ' after that, reload in a moment.' + : assets.reason} {assets.code === 'DISABLED' && ' — set Bridge.AssetsEnabled on the shard to allow it to read its own client files.'}
diff --git a/server/db/schema.sql b/server/db/schema.sql index ab719f9..066dfae 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -957,3 +957,20 @@ UPDATE uo_link_config SET protocol = 8 WHERE id = 1 AND protocol < 8 AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_8_migrated'); INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_8_migrated', '1'); + +-- `shard_spawn_points.unique_id` for an install that already had the table +-- (Asset Bridge phase 9; the column itself is Events phase 12b). +-- +-- The column was added to the CREATE TABLE above and nowhere else, so it reached +-- fresh installs and no existing one -- `CREATE TABLE IF NOT EXISTS` does not add +-- a column to a table that is already there, which is what every ALTER in this +-- file exists to do. `replaceAtlas` inserts `unique_id` unconditionally, so on an +-- upgraded install EVERY spawn-atlas import since v1.2.0 has failed outright with +-- `Unknown column 'unique_id' in 'INSERT INTO'` -- the bestiary, the spawn map and +-- the champion altars all frozen at whatever was last imported. +-- +-- Found by the phase 9 acceptance walk, on a rig whose tables predate 12b: a fresh +-- install cannot reproduce it, and neither can a test whose schema is this file +-- applied to an empty database. That is the same blind spot the protocol-pin block +-- above records, two phases running. +ALTER TABLE shard_spawn_points ADD COLUMN IF NOT EXISTS unique_id VARCHAR(64) NULL;