fix(engagement): four defects the Phase 11b live walk found, and the 26th trigger
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>
This commit is contained in:
46
server/config/clientPaths.js
Normal file
46
server/config/clientPaths.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// ── 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 }
|
||||
@@ -126,6 +126,8 @@ const inapp = (key, name, triggerId, title, body, action, url) => ({
|
||||
|
||||
// ── The sixteen in-universe bodies ─────────────────────────────────────────
|
||||
|
||||
const { PATHS } = require('./clientPaths')
|
||||
|
||||
const TEMPLATES = [
|
||||
// ── The Office of Deeds — houses ────────────────────────────────────────
|
||||
//
|
||||
@@ -185,7 +187,39 @@ const TEMPLATES = [
|
||||
'Thy house has fallen',
|
||||
'{{houseLabel}} has fallen, and the deed is struck from the ledger. The ground is open to any who would build there.',
|
||||
'Review thy holdings',
|
||||
'/player/uo/houses',
|
||||
PATHS.houses,
|
||||
),
|
||||
|
||||
// The one letter this office sends that is not a warning (Phase 11b decision
|
||||
// 11). It is the same clerk and the same ledger, which is the point: an office
|
||||
// that only ever writes when something is wrong teaches a reader to dread its
|
||||
// seal, and the notice that the ledger is set right is the cheapest possible
|
||||
// way not to. It is also why `uo.house.refreshed` is a trigger at all — the
|
||||
// cancellation is the mechanism, this is the message.
|
||||
email(
|
||||
'uo.house.refreshed',
|
||||
'House — refreshed (Office of Deeds)',
|
||||
'uo.house.refreshed',
|
||||
'The ledger is set right for {{houseLabel}}',
|
||||
[
|
||||
heading('h', 'From the Office of Deeds'),
|
||||
text('p1',
|
||||
'This office records that {{houseLabel}}, held in thy name, has been refreshed and '
|
||||
+ 'stands in good repair.{{fromLine}}'),
|
||||
text('p2',
|
||||
'No further notice will be sent concerning it. Should it fall into disrepair again, '
|
||||
+ 'thou wilt hear from us before the deed is touched.'),
|
||||
button('cta', 'Review thy holdings', '{{houseUrl}}', 'Thy holdings are listed here:'),
|
||||
],
|
||||
),
|
||||
inapp(
|
||||
'uo.house.refreshed-inapp',
|
||||
'House — refreshed (in-app)',
|
||||
'uo.house.refreshed',
|
||||
'The Office of Deeds sends word',
|
||||
'{{houseLabel}} has been refreshed and stands in good repair.{{fromLine}}',
|
||||
'Review thy holdings',
|
||||
'{{houseUrl}}',
|
||||
),
|
||||
|
||||
// ── The Merchants' Guild — vendors ──────────────────────────────────────
|
||||
@@ -242,7 +276,7 @@ const TEMPLATES = [
|
||||
'A sale at thy shop',
|
||||
'{{itemLine}} sold for {{price}} gold. The takings are held by thy keeper until thou callest for them.',
|
||||
'Open the market',
|
||||
'/shard/market',
|
||||
PATHS.market,
|
||||
),
|
||||
|
||||
// ── A guild herald ──────────────────────────────────────────────────────
|
||||
@@ -292,7 +326,7 @@ const TEMPLATES = [
|
||||
'{{guildName}} is dissolved',
|
||||
'The charter is void and the rolls are closed. Those who wore its colours wear them no longer.',
|
||||
'Open the shard',
|
||||
'/shard',
|
||||
PATHS.shard,
|
||||
),
|
||||
|
||||
// ── Lord Blackthorn's court — the crown's business ──────────────────────
|
||||
@@ -366,7 +400,7 @@ const TEMPLATES = [
|
||||
text('p1',
|
||||
'{{phaseLabel}} in {{city}}.{{candidateNote}}'),
|
||||
text('p2',
|
||||
'Those who hold the loyalty of the city may speak. Attend before {{autoPickAt}}: '
|
||||
'Those who hold the loyalty of the city may speak. Attend before {{autoPickWhen}}: '
|
||||
+ 'after that hour the matter is decided without thee, and the Crown will hear no '
|
||||
+ 'complaint from any who could have spoken and did not.'),
|
||||
button('cta', 'Attend the city', '{{governorsUrl}}', 'The offices of the realm are recorded here:'),
|
||||
@@ -377,7 +411,7 @@ const TEMPLATES = [
|
||||
'Election — the ballot opens (in-app)',
|
||||
'uo.election.opened',
|
||||
'{{phaseLabel}} in {{city}}',
|
||||
'Attend before {{autoPickAt}} — after that hour the matter is decided without thee.{{candidateNote}}',
|
||||
'Attend before {{autoPickWhen}} — after that hour the matter is decided without thee.{{candidateNote}}',
|
||||
'Attend the city',
|
||||
'{{governorsUrl}}',
|
||||
),
|
||||
@@ -446,7 +480,7 @@ const TEMPLATES = [
|
||||
+ '{{cap}} is the whole of it; there is no further mark to reach.'),
|
||||
text('p2',
|
||||
'What thou dost with it is thine own affair. The guild has taught thee what it knows.'),
|
||||
button('cta', 'Read thy character', '/player/uo/characters'),
|
||||
button('cta', 'Read thy character', PATHS.characters),
|
||||
],
|
||||
),
|
||||
inapp(
|
||||
@@ -456,7 +490,7 @@ const TEMPLATES = [
|
||||
'{{characterName}} has mastered {{skill}}',
|
||||
'Thou hast carried {{skill}} as far as it will be carried — {{cap}} is the whole of it.',
|
||||
'Read thy character',
|
||||
'/player/uo/characters',
|
||||
PATHS.characters,
|
||||
),
|
||||
|
||||
email(
|
||||
@@ -469,7 +503,7 @@ const TEMPLATES = [
|
||||
text('p1',
|
||||
'{{characterName}} has seen {{quest}} through to its end. It is written down, which is '
|
||||
+ 'more than most who set out on it can say.'),
|
||||
button('cta', 'Read thy character', '/player/uo/characters'),
|
||||
button('cta', 'Read thy character', PATHS.characters),
|
||||
],
|
||||
),
|
||||
inapp(
|
||||
@@ -479,7 +513,7 @@ const TEMPLATES = [
|
||||
'{{quest}} — concluded',
|
||||
'{{characterName}} has seen {{quest}} through to its end. It is written down.',
|
||||
'Read thy character',
|
||||
'/player/uo/characters',
|
||||
PATHS.characters,
|
||||
),
|
||||
|
||||
// ── The Chronicler of the Dead ──────────────────────────────────────────
|
||||
@@ -508,7 +542,7 @@ const TEMPLATES = [
|
||||
'{{characterName}} has fallen',
|
||||
'An entry is made in the Chronicle: {{characterName}} has fallen{{slainBy}}.',
|
||||
'Read thy character',
|
||||
'/player/uo/characters',
|
||||
PATHS.characters,
|
||||
),
|
||||
|
||||
email(
|
||||
@@ -532,7 +566,7 @@ const TEMPLATES = [
|
||||
'{{characterName}} was murdered',
|
||||
'An entry is made, and it is not an accident: {{characterName}} was slain{{slainBy}}.',
|
||||
'Read thy character',
|
||||
'/player/uo/characters',
|
||||
PATHS.characters,
|
||||
),
|
||||
|
||||
// ── The keeper of the rolls ─────────────────────────────────────────────
|
||||
@@ -547,7 +581,7 @@ const TEMPLATES = [
|
||||
'The roll of {{boardLabel}} is amended. {{standingLine}}'),
|
||||
text('p2',
|
||||
'A roll is only ever the state of a thing on the day it was read.'),
|
||||
button('cta', 'Read the roll', '/shard/points'),
|
||||
button('cta', 'Read the roll', PATHS.leaderboards),
|
||||
],
|
||||
),
|
||||
inapp(
|
||||
@@ -557,7 +591,7 @@ const TEMPLATES = [
|
||||
'A new name heads {{boardLabel}}',
|
||||
'The roll of {{boardLabel}} is amended. {{standingLine}}',
|
||||
'Read the roll',
|
||||
'/shard/points',
|
||||
PATHS.leaderboards,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -617,7 +651,30 @@ const RULES = [
|
||||
// seconds; mailing them anyway is how a warning system teaches people to
|
||||
// ignore it. Phase 4a's `delay_seconds` + `cancel_on` is precisely this.
|
||||
delay_seconds: 900,
|
||||
cancel_on: ['uo.house.collapsed'],
|
||||
// **Both outcomes, and the refresh is the one the delay is FOR.** A collapse
|
||||
// inside the window makes the warning pointless; a refresh inside it makes
|
||||
// the warning wrong. Phase 11b's live walk found that only the first was
|
||||
// named here, so the good outcome — the player fixing the thing they were
|
||||
// about to be warned about — still produced the letter.
|
||||
cancel_on: ['uo.house.collapsed', 'uo.house.refreshed'],
|
||||
max_sends_per_hour: 200,
|
||||
},
|
||||
{
|
||||
trigger_id: 'uo.house.refreshed',
|
||||
name: 'House — refreshed',
|
||||
audience: 'owner',
|
||||
channels: CHANNELS_OWNER,
|
||||
template_keys: bodies('house.refreshed'),
|
||||
// A day, per house, like the warning it answers — a player refreshing the
|
||||
// same house twice in an afternoon does not need telling twice. No delay:
|
||||
// there is no bad outcome this could be waiting to be overtaken by.
|
||||
//
|
||||
// **This rule is not what does the cancelling.** `cancel_on` is read off the
|
||||
// WARNING's rule and fires whether or not this rule is enabled, so an
|
||||
// operator who wants the cancellation and not the reassurance simply leaves
|
||||
// this one off — which, since every seeded rule ships disabled, is the
|
||||
// default.
|
||||
cooldown_seconds: 86_400,
|
||||
max_sends_per_hour: 200,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -122,7 +122,7 @@ const OWNED_ASSET = [
|
||||
description: 'When it collapses — present ONLY when the shard can state it exactly. Absent is "not knowable", never "not yet read".' },
|
||||
{ name: 'lastRefreshed', type: 'datetime', required: false, example: '2026-08-25T17:21:14Z',
|
||||
description: 'When the house was last refreshed.' },
|
||||
{ name: 'houseUrl', type: 'url', required: false, example: '/shard/houses',
|
||||
{ name: 'houseUrl', type: 'url', required: false, example: '/uo/houses',
|
||||
description: 'Site-relative path to the IDOC page.' },
|
||||
{ name: 'houseLabel', type: 'string', required: false, example: '“The Silver Anvil”, in Britain',
|
||||
description: 'A label: the house\'s name in quotes with its region, or its seal number when it has no name.' },
|
||||
@@ -156,6 +156,47 @@ const OWNED_ASSET = [
|
||||
description: 'A whole detail line, assembled from the parts the frame actually carried.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// **The good outcome, and it exists because a delay without a cancel is just
|
||||
// a late mail** (ENGAGEMENT.md §4.2a). `uo.house.idoc_warning` ships
|
||||
// `delay_seconds: 900` so an owner who repairs the house inside the window is
|
||||
// never told it is in peril — and until Phase 11b's live walk there was
|
||||
// nothing that could cancel it: the mapper returned early on every transition
|
||||
// that was not a late stage, so a refresh reached the engine as silence. The
|
||||
// wire already carried the transition; only this declaration was missing.
|
||||
//
|
||||
// It is a real notification as well as a cancel signal (decision 11), so it
|
||||
// carries the labels a body needs rather than the serial alone.
|
||||
id: 'uo.house.refreshed',
|
||||
label: 'Your house was refreshed',
|
||||
description: 'One of your houses was refreshed and is out of danger. Cancels a pending decay warning.',
|
||||
kind: 'event',
|
||||
// The SAME subject as the warning it cancels, and that is load-bearing rather
|
||||
// than tidy: `outboxDb.cancel` matches on (rule, subject_key), so a refresh
|
||||
// whose subject were anything else would cancel nothing.
|
||||
subjectKey: 'houseSerial',
|
||||
audience: 'owner',
|
||||
ceiling: 'owner',
|
||||
version: V1,
|
||||
variables: [
|
||||
{ name: 'houseSerial', type: 'string', required: true, example: '0x400142F9',
|
||||
description: 'The house, as the shard names it. Also the cooldown subject, and what the cancellation matches on.' },
|
||||
{ name: 'houseName', type: 'string', required: false, example: 'Millrace',
|
||||
description: 'The house sign\'s name, when it has one.' },
|
||||
{ name: 'previousStage', type: 'string', required: false, example: 'Greatly',
|
||||
description: 'The decay stage it was in before it was refreshed.' },
|
||||
{ name: 'region', type: 'string', required: false, example: 'Britain',
|
||||
description: 'The named region the house stands in.' },
|
||||
{ name: 'location', type: 'string', required: false, example: 'Felucca 1480, 1600',
|
||||
description: 'Facet and coordinates, already formatted for reading.' },
|
||||
{ name: 'houseUrl', type: 'url', required: false, example: '/uo/houses',
|
||||
description: 'Site-relative path to the housing page.' },
|
||||
{ name: 'houseLabel', type: 'string', required: false, example: '“The Silver Anvil”, in Britain',
|
||||
description: 'A label: the house\'s name in quotes with its region, or its seal number when it has no name.' },
|
||||
{ name: 'fromLine', type: 'string', required: false, example: ' It stood greatly worn.',
|
||||
description: 'A trailing fragment naming the stage it was rescued from. Leads with its own space, and is empty when the frame carried no previous stage.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'uo.vendor.expiring',
|
||||
label: 'Your vendor is about to be dismissed',
|
||||
@@ -189,7 +230,7 @@ const OWNED_ASSET = [
|
||||
description: 'What each tick deducts.' },
|
||||
{ name: 'location', type: 'string', required: false, example: 'Trammel 1421, 1699 (Britain)',
|
||||
description: 'Where the shop stands, already formatted for reading.' },
|
||||
{ name: 'marketUrl', type: 'url', required: false, example: '/shard/market',
|
||||
{ name: 'marketUrl', type: 'url', required: false, example: '/uo/market',
|
||||
description: 'Site-relative path to the market page.' },
|
||||
{ name: 'shopLabel', type: 'string', required: false, example: 'thy shop “The Silver Anvil”',
|
||||
description: 'A label: the shop named, or simply \'thy vendor\' when it has no name.' },
|
||||
@@ -388,7 +429,7 @@ const SOCIAL_CIVIC = [
|
||||
// is optional because a member the sweep never saw has no row there.
|
||||
{ name: 'memberName', type: 'string', required: false, example: 'Bran',
|
||||
description: 'Who left, when the roster mirror still knows their name.' },
|
||||
{ name: 'guildUrl', type: 'url', required: false, example: '/shard/guilds',
|
||||
{ name: 'guildUrl', type: 'url', required: false, example: '/uo/guilds/1042',
|
||||
description: 'Site-relative path to the guilds page.' },
|
||||
{ name: 'memberLabel', type: 'string', required: false, example: 'Aldric',
|
||||
description: 'A label: the departing member\'s name, or \'A member\' when the roster mirror has no name for them.' },
|
||||
@@ -440,7 +481,7 @@ const SOCIAL_CIVIC = [
|
||||
description: 'Your character\'s name, as the city knows it.' },
|
||||
{ name: 'previousGovernorName', type: 'string', required: false, example: 'Mireille',
|
||||
description: 'Who held the seat before, when there was someone.' },
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/shard/governors',
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/uo/governors',
|
||||
description: 'Site-relative path to the governors page.' },
|
||||
{ name: 'inSuccessionTo', type: 'string', required: false, example: ' in succession to Mireille',
|
||||
description: 'A trailing fragment, LEADING SPACE included. Empty today: the frame names no outgoing governor.' },
|
||||
@@ -462,7 +503,7 @@ const SOCIAL_CIVIC = [
|
||||
description: 'The new governor.' },
|
||||
{ name: 'previousGovernorName', type: 'string', required: false, example: 'Mireille',
|
||||
description: 'Who held the seat before, when there was someone.' },
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/shard/governors',
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/uo/governors',
|
||||
description: 'Site-relative path to the governors page.' },
|
||||
{ name: 'inSuccessionTo', type: 'string', required: false, example: ' in succession to Mireille',
|
||||
description: 'A trailing fragment, LEADING SPACE included. Empty today: the frame names no outgoing governor.' },
|
||||
@@ -489,9 +530,16 @@ const SOCIAL_CIVIC = [
|
||||
description: 'Which phase opened: nominate or vote.' },
|
||||
{ name: 'autoPickAt', type: 'datetime', required: true, example: '2026-09-04T00:00:00Z',
|
||||
description: 'When the game decides for itself — the real deadline.' },
|
||||
// The same instant a person can read. A `datetime` renders as the string the
|
||||
// payload holds and core has no interpolation filters by design, so a body
|
||||
// that interpolates the machine value prints an ISO-8601 stamp mid-sentence.
|
||||
// The machine value STAYS — an operator writes `is at most` conditions
|
||||
// against it — and the body uses this one.
|
||||
{ name: 'autoPickWhen', type: 'string', required: false, example: '4 September 2026, 00:00 UTC',
|
||||
description: 'The deadline as prose, for a body. `autoPickAt` remains the machine value a condition compares.' },
|
||||
{ name: 'candidates', type: 'int', required: false, example: 3,
|
||||
description: 'How many candidates stand.' },
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/shard/governors',
|
||||
{ name: 'governorsUrl', type: 'url', required: false, example: '/uo/governors',
|
||||
description: 'Site-relative path to the governors page.' },
|
||||
{ name: 'phaseLabel', type: 'string', required: false, example: 'The ballot is open',
|
||||
description: 'The phase as a clause rather than as the wire\'s enum.' },
|
||||
@@ -522,7 +570,7 @@ const COME_ONLINE = [
|
||||
description: 'champion, mini or sea.' },
|
||||
{ name: 'location', type: 'string', required: false, example: 'Felucca 5187, 570',
|
||||
description: 'Where, already formatted for reading.' },
|
||||
{ name: 'champsUrl', type: 'url', required: false, example: '/shard/champs',
|
||||
{ name: 'champsUrl', type: 'url', required: false, example: '/uo/champs',
|
||||
description: 'Site-relative path to the champions page.' },
|
||||
{ name: 'atPlace', type: 'string', required: false, example: ' at Felucca 1480, 1600 (Destard)',
|
||||
description: 'A trailing fragment, LEADING SPACE included, or empty when the frame carries no location.' },
|
||||
@@ -546,7 +594,7 @@ const COME_ONLINE = [
|
||||
description: 'The boss, when the shard names it.' },
|
||||
{ name: 'location', type: 'string', required: false, example: 'Felucca 5187, 570',
|
||||
description: 'Where, already formatted for reading.' },
|
||||
{ name: 'champsUrl', type: 'url', required: false, example: '/shard/champs',
|
||||
{ name: 'champsUrl', type: 'url', required: false, example: '/uo/champs',
|
||||
description: 'Site-relative path to the champions page.' },
|
||||
{ name: 'atPlace', type: 'string', required: false, example: ' at Felucca 1480, 1600 (Destard)',
|
||||
description: 'A trailing fragment, LEADING SPACE included, or empty when the frame carries no location.' },
|
||||
@@ -569,7 +617,7 @@ const COME_ONLINE = [
|
||||
variables: [
|
||||
{ name: 'shardName', type: 'string', required: false, example: 'UOMysticmoon',
|
||||
description: 'What the shard calls itself.' },
|
||||
{ name: 'statusUrl', type: 'url', required: false, example: '/shard',
|
||||
{ name: 'statusUrl', type: 'url', required: false, example: '/uo/shard',
|
||||
description: 'Site-relative path to the shard status page.' },
|
||||
],
|
||||
},
|
||||
@@ -586,7 +634,7 @@ const COME_ONLINE = [
|
||||
description: 'What the shard calls itself.' },
|
||||
{ name: 'clean', type: 'boolean', required: false, example: true,
|
||||
description: 'Whether it was a clean shutdown rather than a crash.' },
|
||||
{ name: 'statusUrl', type: 'url', required: false, example: '/shard',
|
||||
{ name: 'statusUrl', type: 'url', required: false, example: '/uo/shard',
|
||||
description: 'Site-relative path to the shard status page.' },
|
||||
],
|
||||
},
|
||||
@@ -655,7 +703,7 @@ const STAFF_FACING = [
|
||||
description: 'What they wrote.' },
|
||||
{ name: 'location', type: 'string', required: false, example: 'Trammel 1421, 1699',
|
||||
description: 'Where they are, already formatted for reading.' },
|
||||
{ name: 'pagesUrl', type: 'url', required: false, example: '/admin/shard',
|
||||
{ name: 'pagesUrl', type: 'url', required: false, example: '/admin/uo/ops',
|
||||
description: 'Site-relative path to the help-page queue.' },
|
||||
],
|
||||
},
|
||||
@@ -734,7 +782,7 @@ const OPERATOR_FACING = [
|
||||
description: 'The threshold it crossed.' },
|
||||
{ name: 'direction', type: 'string', required: true, example: 'up',
|
||||
description: 'up or down.' },
|
||||
{ name: 'economyUrl', type: 'url', required: false, example: '/shard',
|
||||
{ name: 'economyUrl', type: 'url', required: false, example: '/uo/shard',
|
||||
description: 'Site-relative path to the shard status page.' },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user