1- // SPDX-License-Identifier: AGPL-3.0-or-later
2-
3- /**
4- * Historian REST routes.
5- *
6- * Provides endpoints for querying historical co-simulation data stored
7- * in TimescaleDB, including time-series telemetry, session metadata,
8- * and replay control.
9- */
10-
1+ import type { CosimMqttClient } from "@modelscript/cosim" ;
2+ import { HistorianReplayer } from "@modelscript/cosim" ;
113import express from "express" ;
124import type { Pool } from "pg" ;
135
@@ -23,11 +15,15 @@ interface HistorianQueryParams {
2315 interval ?: string ;
2416}
2517
18+ /** Active replayer tracking. */
19+ const activeReplayers = new Map < string , HistorianReplayer > ( ) ;
20+
2621/**
2722 * Create the historian router.
2823 * @param pool PostgreSQL/TimescaleDB connection pool (null = stubs)
24+ * @param mqttClient MQTT client for replay publishing (null = stub responses)
2925 */
30- export function historianRouter ( pool : Pool | null ) : express . Router {
26+ export function historianRouter ( pool : Pool | null , mqttClient ?: CosimMqttClient | null ) : express . Router {
3127 const router = express . Router ( ) ;
3228
3329 // GET /api/v1/historian/sessions — List recorded sessions
@@ -174,8 +170,9 @@ export function historianRouter(pool: Pool | null): express.Router {
174170 variable_name : string ;
175171 value : number ;
176172 } [ ] ) {
177- if ( ! values [ row . participant_id ] ) values [ row . participant_id ] = { } ;
178- values [ row . participant_id ] [ row . variable_name ] = row . value ;
173+ const pid = row . participant_id ;
174+ values [ pid ] = values [ pid ] ?? { } ;
175+ values [ pid ] [ row . variable_name ] = row . value ;
179176 }
180177
181178 res . json ( { sessionId : query . sessionId , values } ) ;
@@ -256,25 +253,68 @@ export function historianRouter(pool: Pool | null): express.Router {
256253 return res . status ( 400 ) . json ( { error : "Missing required field: sessionId" } ) ;
257254 }
258255
256+ if ( ! pool || ! mqttClient ) {
257+ return res . status ( 503 ) . json ( { error : "Historian replay requires TimescaleDB and MQTT" } ) ;
258+ }
259+
259260 const replayId = `replay-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 6 ) } ` ;
260- res . json ( { ok : true , replayId, sessionId, from : from ?? null , to : to ?? null , speedFactor, state : "playing" } ) ;
261+ const replayer = new HistorianReplayer ( pool , mqttClient ) ;
262+ activeReplayers . set ( replayId , replayer ) ;
263+
264+ // Start replay asynchronously
265+ const replayFrom = from ? new Date ( from ) : new Date ( 0 ) ;
266+ const replayTo = to ? new Date ( to ) : new Date ( ) ;
267+
268+ void replayer
269+ . replay ( {
270+ sessionId,
271+ from : replayFrom ,
272+ to : replayTo ,
273+ speedFactor,
274+ } )
275+ . then ( ( ) => {
276+ // Auto-cleanup on completion
277+ if ( replayer . state === "completed" || replayer . state === "error" ) {
278+ activeReplayers . delete ( replayId ) ;
279+ }
280+ } ) ;
281+
282+ res . json ( { ok : true , replayId, sessionId, speedFactor, state : replayer . state } ) ;
261283 } ) ;
262284
263- // POST /api/v1/historian/replay/pause — Pause replay
264- router . post ( "/replay/pause" , ( _req , res ) => {
265- res . json ( { ok : true , state : "paused" } ) ;
285+ // POST /api/v1/historian/replay/:id/pause — Pause replay
286+ router . post ( "/replay/:id/pause" , ( req , res ) => {
287+ const replayer = activeReplayers . get ( req . params [ "id" ] ?? "" ) ;
288+ if ( ! replayer ) return res . status ( 404 ) . json ( { error : "Replay not found" } ) ;
289+ replayer . pause ( ) ;
290+ res . json ( { ok : true , state : replayer . state } ) ;
266291 } ) ;
267292
268- // POST /api/v1/historian/replay/resume — Resume replay
269- router . post ( "/replay/resume" , ( _req , res ) => {
270- res . json ( { ok : true , state : "playing" } ) ;
293+ // POST /api/v1/historian/replay/:id/resume — Resume replay
294+ router . post ( "/replay/:id/resume" , ( req , res ) => {
295+ const replayer = activeReplayers . get ( req . params [ "id" ] ?? "" ) ;
296+ if ( ! replayer ) return res . status ( 404 ) . json ( { error : "Replay not found" } ) ;
297+ replayer . resume ( ) ;
298+ res . json ( { ok : true , state : replayer . state } ) ;
271299 } ) ;
272300
273- // POST /api/v1/historian/replay/stop — Stop replay
274- router . post ( "/replay/stop" , ( _req , res ) => {
301+ // POST /api/v1/historian/replay/:id/stop — Stop replay
302+ router . post ( "/replay/:id/stop" , ( req , res ) => {
303+ const replayId = req . params [ "id" ] ?? "" ;
304+ const replayer = activeReplayers . get ( replayId ) ;
305+ if ( ! replayer ) return res . status ( 404 ) . json ( { error : "Replay not found" } ) ;
306+ replayer . stop ( ) ;
307+ activeReplayers . delete ( replayId ) ;
275308 res . json ( { ok : true , state : "idle" } ) ;
276309 } ) ;
277310
311+ // GET /api/v1/historian/replay/:id — Get replay status
312+ router . get ( "/replay/:id" , ( req , res ) => {
313+ const replayer = activeReplayers . get ( req . params [ "id" ] ?? "" ) ;
314+ if ( ! replayer ) return res . status ( 404 ) . json ( { error : "Replay not found" } ) ;
315+ res . json ( { state : replayer . state , error : replayer . error } ) ;
316+ } ) ;
317+
278318 // DELETE /api/v1/historian/sessions/:id — Delete recorded session data
279319 router . delete ( "/sessions/:id" , async ( req , res ) => {
280320 const sessionId = req . params [ "id" ] ?? "" ;
0 commit comments