Home

M5.1 – Train announcements panel

Implementation plan — Train announcements panel (M5.1)

Frontend-only slice: a static list of station PA messages in the interlocking view, played locally on the signalman's device.

Specification:

Starting point

Confirmed design decisions

  1. Local playback only — clicking a row plays Ogg audio on the clicking browser tab. No WebSocket, Redis, SQLite or audit log.
  2. Static manifestweb/src/config/trainAnnouncements.ts holds the catalogue. Keyed by interlocking name (stable across DB re-seeds; the panel already knows the name from the interlocking header fetch). A "default" key provides the fallback list.
  3. Stable soundKey — kebab-case identifier maps 1:1 to {soundKey}.ogg under /sounds/train-announcements/.
  4. Labels via i18n — manifest entries carry labelKey; UI resolves t(\trainAnnouncements:${labelKey}`)`.
  5. Panel placement — third column on wide screens (Radio | Składy | Zapowiedzi); third tab on narrow screens.
  6. Distinct from radio — separate sound directory, no protocol overlap.

Implementation stages

Two stages — both frontend.

Stage 1 ──► Stage 2
 panel +     assets +
 manifest    polish
Stage Focus Delivers
1 Panel + manifest + hook Static config, useTrainAnnouncementSound, <InterlockingTrainAnnouncementsPanel>, three-column layout, i18n
2 Assets & acceptance .ogg files, optional playing-row highlight, manual walkthrough

Stage 1 — Panel, manifest, hook

Static manifest

web/src/config/trainAnnouncements.ts

export type TrainAnnouncementEntry = {
  soundKey: string;
  labelKey: string;
};

/** Interlocking name → ordered announcement list. Use "default" as fallback. */
export const TRAIN_ANNOUNCEMENTS: Record<string, TrainAnnouncementEntry[]> = {
  default: [
    { soundKey: "track-1-freight", labelKey: "track1Freight" },
    { soundKey: "departure-gluszyca", labelKey: "departureGluszyca" },
    { soundKey: "departure-wroclaw", labelKey: "departureWroclaw" },
    { soundKey: "departure-warszawa-centralna", labelKey: "departureWarszawaCentralna" },
  ],
  // "Wałbrzych Główny": [ … dedicated list … ],
};

export function trainAnnouncementsFor(interlockingName: string): TrainAnnouncementEntry[] {
  return TRAIN_ANNOUNCEMENTS[interlockingName] ?? TRAIN_ANNOUNCEMENTS.default ?? [];
}

To customise a specific box later, add a named key alongside "default".

Hook

web/src/hooks/useTrainAnnouncementSound.ts

Component

web/src/components/interlocking/InterlockingTrainAnnouncementsPanel.tsx

Page integration

pages/InterlockingPage.tsx (InterlockingStaffedWorkArea):

i18n

Stage 1 done when: staffed interlocking shows the third panel; clicking a row triggers local playback (file may be missing yet).


Stage 2 — Assets & polish

Assets

Add .ogg files under web/public/sounds/train-announcements/ matching the soundKey values in the manifest.

UX polish (optional)

Manual acceptance walkthrough

  1. Occupy an interlocking — third panel / tab visible.
  2. Click each entry — audio only on that device.
  3. Second browser on same box — clicks stay local.
  4. Chat panel unaffected.
  5. Rename interlocking to a name absent from manifest with no "default" key removed — empty-state hint (only if testing edge case).

Out of scope

File checklist

Area File
Config web/src/config/trainAnnouncements.ts
Hook web/src/hooks/useTrainAnnouncementSound.ts
Component web/src/components/interlocking/InterlockingTrainAnnouncementsPanel.tsx
Page web/src/pages/InterlockingPage.tsx
i18n web/src/i18n/locales/{pl,en}/trainAnnouncements.json
i18n web/src/i18n/locales/{pl,en}/interlocking.json (panel keys)
Assets web/public/sounds/train-announcements/*.ogg