diff --git a/server/src/model/events/eventPublic.model.js b/server/src/model/events/eventPublic.model.js index 5b5818f..ee070e1 100644 --- a/server/src/model/events/eventPublic.model.js +++ b/server/src/model/events/eventPublic.model.js @@ -92,16 +92,22 @@ const isLive = (status) => publicStatus(status) === 'live' /** * The label of the phase a run is in, resolved from the PINNED version's spec. * - * A phase id is a slug an author typed and the label is what they meant it to - * read as, so a page rendering the id would show `phase-2` to the public. A + * A phase key is a slug an author typed and the label is what they meant it to + * read as, so a page rendering the key would show `phase-2` to the public. A * phase the spec does not name answers null and the page shows nothing, which is * the right answer for a version edited since: the run pinned the old spec and * the old spec is what it is executing. + * + * **A run's `current_phase` is the phase's `key`**, which is what `spec.js` + * validates and the engine writes. This matched `id` until RunicNPC stage 9 + * (D314), so every live run read "Under way" since Events Phase 14a; `id` is + * still accepted second, for a spec stored in that older shape. */ -function phaseLabel(spec, phaseId) { - if (!phaseId || !spec || !Array.isArray(spec.phases)) return null - const phase = spec.phases.find((p) => p && p.id === phaseId) - return (phase && (phase.label || phase.id)) || null +function phaseLabel(spec, phaseKey) { + if (!phaseKey || !spec || !Array.isArray(spec.phases)) return null + const phase = + spec.phases.find((p) => p && p.key === phaseKey) || spec.phases.find((p) => p && p.id === phaseKey) + return (phase && (phase.label || phase.key || phase.id)) || null } /** diff --git a/server/test/eventPublic.test.js b/server/test/eventPublic.test.js index c0437a8..e518205 100644 --- a/server/test/eventPublic.test.js +++ b/server/test/eventPublic.test.js @@ -40,8 +40,9 @@ const NOW = new Date('2026-09-01T12:00:00Z') const SPEC = { schedule: { kind: 'manual' }, phases: [ - { id: 'muster', label: 'The muster', steps: [{ id: 's1', action: 'core.announce' }] }, - { id: 'assault', label: 'The assault', steps: [] }, + // `key`, as spec.js validates and every stored spec has it (D314). + { key: 'muster', label: 'The muster', steps: [{ id: 's1', action: 'core.announce' }] }, + { key: 'assault', label: 'The assault', steps: [] }, ], } @@ -368,6 +369,24 @@ test('only the live occurrence carries progress, asked for the reader (1.12.0)', } }) +test('phaseLabel finds a phase by key first, still accepts id, and never shows a step', () => { + const { phaseLabel } = publicModel + // A UO event's shape: keyed phases, steps of UO's actions. + const uo = { + phases: [ + { key: 'gather', label: 'The gathering', steps: [{ action: 'uo.creature.spawn' }] }, + { key: 'siege', label: 'Siege of Yew', steps: [] }, + ], + } + assert.equal(phaseLabel(uo, 'gather'), 'The gathering') + assert.equal(phaseLabel(uo, 'siege'), 'Siege of Yew') + assert.equal(phaseLabel({ phases: [{ id: 'old', label: 'An older shape' }] }, 'old'), 'An older shape') + // A key and an id both matching: the key wins, because the key is what the engine writes. + assert.equal(phaseLabel({ phases: [{ id: 'x', label: 'By id' }, { key: 'x', label: 'By key' }] }, 'x'), 'By key') + assert.equal(phaseLabel(uo, 'nope'), null) + assert.equal(phaseLabel(uo, null), null) +}) + test('a phase the pinned version does not name renders nothing rather than an id', async () => { store.runs[0].status = 'running' store.runs[0].current_phase = 'a-phase-since-renamed'