Needs website#<core> (the cooldown key and the seed-rule ceiling). 1. Every owner-audienced trigger reached NOBODY. `resolveTarget` read `link.user_id`; the model's `toSafe` returns `userId`. So the whole flagship family -- houses, vendors, logins, unlinks, deaths, the governor's letter -- resolved to null and looked exactly like the ordinary unlinked-account case, which the code treats as normal and deliberately does not log. The test fake returned `user_id` and therefore agreed with the bug, while `shardStreams.test.js`'s fake next door -- same model, the path this file says it copies -- returned `userId`. The fake is now built by running the real `toSafe` over a stubbed db row, so the shape is not a hand-written opinion. 2. `uo.house.refreshed`, the 26th trigger (the org lead's decision 11). The warning's rule carries `delay_seconds: 900` so a player who repairs the house inside the quarter-hour is never told it is in peril -- and nothing could cancel it: `cancel_on` named only the collapse. The wire had carried the transition all along; the mapper returned early on it. It fires on `Ageless` as well as `LikeNew`, and `Ageless` is the common case: a condemned house cannot be refreshed at all (`RefreshDecay()` refuses `DecayType.Condemned`), so the rescue is the owner logging in, and their newest house then reads `Ageless`. Ships a body and a seeded (disabled) rule of its own; the cancellation is read off the WARNING's rule and works whether or not the new one is enabled. 3. Every call-to-action in every in-universe body was a dead link, from two independent mistakes. The client router prefixes a module's routes with its ID (`/uo/houses`), not with module.json's `mounts` (`/shard/...`), so every declared `example` was a 404 -- and an example is what the template editor previews and test-sends with. And no `url` variable was ever populated by the mapper, so the buttons rendered with an empty href and dropped out of the text part entirely. Both now read `config/clientPaths.js`. Two tests close it. 4. A raw wire timestamp was signing off the Merchants' Guild's letter (`2026-09-02T04:06:43.8397548Z`, mid-sentence). Core has no interpolation filters by design, so the readable form is assembled in the mapper and arrives as its own variable; the machine value stays, because an operator writes `is at most` conditions against it. Also fixes a latent flake: `hoursRemaining` floors a live clock, so a fixture at a whole number asserted 19 or 20 depending on sub-millisecond timing. 527 module tests green (3 new). Proved end to end against real ServUO + the release sidecar + a live SMTP catcher; see docs#<docs>. Co-Authored-By: Claude <noreply@anthropic.com>
47 lines
2.2 KiB
JavaScript
47 lines
2.2 KiB
JavaScript
// ── The module's own client paths, in one place ────────────────────────────
|
|
//
|
|
// Every link a notification puts in front of a player is a path into this
|
|
// module's SPA routes, and Phase 11b's live walk found that not one of them was
|
|
// right: the declared examples all read `/shard/…` (module.json's `mounts`), the
|
|
// bodies hard-coded a mixture of `/shard/…` and `/player/uo/…`, and the mapper
|
|
// populated none of the URL variables at all — so every in-universe letter shipped
|
|
// with an empty href and every template preview showed a dead one.
|
|
//
|
|
// **The prefix is the module ID, not the mount.** `registry.registerRoutes`
|
|
// prefixes a module's client routes with `<id>/` and nothing else
|
|
// (`client/src/modules/registry.js`), which is why `module.json`'s `mounts` is not
|
|
// the answer — that field says what the module CLAIMS, and the router says where
|
|
// it landed. `client/src/entry.jsx`'s own `registerNav` is the check: the hrefs it
|
|
// gives the sidebar are these, and if the two ever disagree the sidebar is right.
|
|
//
|
|
// Kept server-side and shared by BOTH the trigger declarations (their `example`s,
|
|
// which the template editor previews and test-sends with) and the seeded bodies,
|
|
// so a route that moves is one edit rather than thirty.
|
|
|
|
const ID = 'uo'
|
|
|
|
const PATHS = {
|
|
shard: `/${ID}/shard`,
|
|
champs: `/${ID}/champs`,
|
|
guilds: `/${ID}/guilds`,
|
|
governors: `/${ID}/governors`,
|
|
houses: `/${ID}/houses`,
|
|
atlas: `/${ID}/atlas`,
|
|
leaderboards: `/${ID}/leaderboards`,
|
|
market: `/${ID}/market`,
|
|
// Self-service and staff areas sit under core's own wrappers, so they carry
|
|
// core's prefix as well as the module's.
|
|
characters: `/player/${ID}/characters`,
|
|
ops: `/admin/${ID}/ops`,
|
|
}
|
|
|
|
/** One guild's roster, when the frame names a guild; the list otherwise. */
|
|
const guildPath = (guildId) =>
|
|
(guildId === undefined || guildId === null ? PATHS.guilds : `${PATHS.guilds}/${guildId}`)
|
|
|
|
/** One vendor's page, when the frame names one; the market otherwise. */
|
|
const vendorPath = (serial) =>
|
|
(serial ? `${PATHS.market}/vendors/${serial}` : PATHS.market)
|
|
|
|
module.exports = { PATHS, guildPath, vendorPath }
|