* create report service

* write report from runtimeState

* use in UI

* clear report

* update types

* clear all from settings menu

* rearence rightclik menu

* refactor styling

* also report roll events

* ontime/under time is same colour

* refactor reporter

* add target to ontime-refetch

* remove menu

* fectch only on message from server

* memo useGetEventReport

* refactor

* use staleTime

* dont add to menu yet

* implement review

* clear all from menu

* fix merge

* start end show

* combine test for the go button text and action

* add report to menu

* extract csv utility

* report management

* unneeded async

* small refacort of triggerReportEntry

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2025-02-23 17:02:09 +01:00
committed by GitHub
parent 21877e4bff
commit b6507c27a6
35 changed files with 542 additions and 72 deletions
+2
View File
@@ -11,6 +11,7 @@ import { router as sheetsRouter } from './sheets/sheets.router.js';
import { router as excelRouter } from './excel/excel.router.js';
import { router as sessionRouter } from './session/session.router.js';
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
import { router as reportRouter } from './report/report.router.js';
export const appRouter = express.Router();
@@ -25,6 +26,7 @@ appRouter.use('/excel', excelRouter);
appRouter.use('/url-presets', urlPresetsRouter);
appRouter.use('/session', sessionRouter);
appRouter.use('/view-settings', viewSettingsRouter);
appRouter.use('/report', reportRouter);
//we don't want to redirect to react index when using api routes
appRouter.all('/*', (_req, res) => {
@@ -0,0 +1,18 @@
import type { Request, Response } from 'express';
import type { OntimeReport } from 'ontime-types';
import * as report from './report.service.js';
export function getAll(_req: Request, res: Response<OntimeReport>) {
res.json(report.generate());
}
export function deleteAll(_req: Request, res: Response<OntimeReport>) {
report.clear();
res.status(200).send();
}
export function deleteWithId(req: Request, res: Response<OntimeReport>) {
const { eventId } = req.params;
report.clear(eventId);
res.status(200).send();
}
@@ -0,0 +1,10 @@
import express from 'express';
import { getAll, deleteWithId, deleteAll } from './report.controller.js';
import { paramsMustHaveEventId } from '../rundown/rundown.validation.js';
export const router = express.Router();
router.get('/', getAll);
router.delete('/all', deleteAll);
router.delete('/:eventId', paramsMustHaveEventId, deleteWithId);
@@ -0,0 +1,65 @@
import { OntimeReport, OntimeEventReport, TimerLifeCycle } from 'ontime-types';
import { RuntimeState } from '../../stores/runtimeState.js';
import { sendRefetch } from '../../adapters/websocketAux.js';
import { DeepReadonly } from 'ts-essentials';
const report = new Map<string, OntimeEventReport>();
let formattedReport: OntimeReport | null = null;
/**
* generates a full report
* @returns full report
*/
export function generate(): OntimeReport {
if (formattedReport === null) {
formattedReport = Object.fromEntries(report);
}
return formattedReport;
}
/**
* clear report
* @param id optional id of a event report to clear
*/
export function clear(id?: string) {
formattedReport = null;
if (id) {
report.delete(id);
} else {
report.clear();
}
}
/**
* trigger report entry
* @param cycle
* @param state
* @returns
*/
export function triggerReportEntry(
cycle: TimerLifeCycle.onStart | TimerLifeCycle.onStop,
state: DeepReadonly<RuntimeState>,
) {
if (!state.eventNow?.id) {
return;
}
const eventId = state.eventNow.id;
if (cycle === TimerLifeCycle.onStart) {
report.set(eventId, { startedAt: state.timer.startedAt, endedAt: null });
formattedReport = null;
return;
}
if (cycle === TimerLifeCycle.onStop) {
const startedAt = report.get(eventId)?.startedAt ?? null;
report.set(eventId, { startedAt, endedAt: state.clock });
formattedReport = null;
sendRefetch({
target: 'REPORT',
});
return;
}
}
@@ -266,6 +266,7 @@ function notifyChanges(options: NotifyChangesOptions) {
if (options.external) {
// advice socket subscribers of change
const payload = {
target: 'RUNDOWN',
changes: Array.isArray(options.timer) ? options.timer : undefined,
reload: options.reload,
revision: cache.getMetadata().revision,
@@ -20,6 +20,8 @@ import type { RuntimeState } from '../../stores/runtimeState.js';
import { timerConfig } from '../../config/config.js';
import { eventStore } from '../../stores/EventStore.js';
import { triggerReportEntry } from '../../api-data/report/report.service.js';
import { EventTimer } from '../EventTimer.js';
import { RestorePoint, restoreService } from '../RestoreService.js';
import {
@@ -288,14 +290,17 @@ class RuntimeService {
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
return false;
}
const previousState = runtimeState.getState();
const rundown = getRundown();
const success = runtimeState.load(event, rundown, initialData);
if (success) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
const newState = runtimeState.getState();
process.nextTick(() => {
triggerAutomations(TimerLifeCycle.onLoad, runtimeState.getState());
triggerReportEntry(TimerLifeCycle.onStop, previousState);
triggerAutomations(TimerLifeCycle.onLoad, newState);
});
}
return success;
@@ -474,6 +479,7 @@ class RuntimeService {
if (didStart) {
process.nextTick(() => {
triggerReportEntry(TimerLifeCycle.onStart, newState);
triggerAutomations(TimerLifeCycle.onStart, newState);
});
}
@@ -536,8 +542,8 @@ class RuntimeService {
*/
@broadcastResult
public stop(): boolean {
const state = runtimeState.getState();
const canStop = validatePlayback(state.timer.playback, state.timer.phase).stop;
const previousState = runtimeState.getState();
const canStop = validatePlayback(previousState.timer.playback, previousState.timer.phase).stop;
if (!canStop) {
return false;
}
@@ -546,6 +552,7 @@ class RuntimeService {
const newState = runtimeState.getState();
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
process.nextTick(() => {
triggerReportEntry(TimerLifeCycle.onStop, previousState);
triggerAutomations(TimerLifeCycle.onStop, newState);
});
@@ -598,12 +605,14 @@ class RuntimeService {
if (result.eventId !== previousState.eventNow?.id) {
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
process.nextTick(() => {
triggerReportEntry(TimerLifeCycle.onStop, previousState);
triggerAutomations(TimerLifeCycle.onLoad, newState);
});
}
if (result.didStart) {
process.nextTick(() => {
triggerReportEntry(TimerLifeCycle.onStart, newState);
triggerAutomations(TimerLifeCycle.onStart, newState);
});
}
-2
View File
@@ -391,7 +391,6 @@ export function start(state: RuntimeState = runtimeState): boolean {
// update offset
state.runtime.offset = getRuntimeOffset(state);
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
return true;
}
@@ -410,7 +409,6 @@ export function stop(state: RuntimeState = runtimeState): boolean {
if (state.timer.playback === Playback.Stop) {
return false;
}
clear();
runtimeState.runtime.actualStart = null;
runtimeState.runtime.expectedEnd = null;