From 18b9b9dd570f3c71062f323ef638d0f9d1196bb2 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Wed, 22 Jan 2025 15:05:45 +0100 Subject: [PATCH 01/20] create report service --- .../src/api-data/report/report.service.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 apps/server/src/api-data/report/report.service.ts diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts new file mode 100644 index 0000000000..37459ca5df --- /dev/null +++ b/apps/server/src/api-data/report/report.service.ts @@ -0,0 +1,82 @@ +import { OntimeReport, OntimeReportData } from 'ontime-types'; +import { RuntimeState } from '../../stores/runtimeState.js'; + +//TODO: there seams to be some actions that should invalidate reports +// events timer edits? +// event delete + +const report = new Map(); + +let formattedReport: OntimeReport | null = null; + +/** + * blank placeholder data + */ +const blankReportData: OntimeReportData = { + startAt: null, + endAt: null, +} as const; + +/** + * generates a full report + * @returns full report + */ +export function generate(): OntimeReport { + if (formattedReport === null) { + formattedReport = Object.fromEntries(report); + } + return formattedReport; +} + +export function getWithId(id: string): OntimeReportData | null { + return report.get(id) ?? null; +} + +/** + * 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(); + } +} + +export function eventStart(state: RuntimeState) { + formattedReport = null; + if (state.eventNow === null) { + // eslint-disable-next-line no-unused-labels -- dev code path + DEV: { + throw new Error('report.eventStart: called without eventNow present'); + } + return; + } + + // this clears out potentaly old data + report.set(state.eventNow.id, { ...blankReportData, startAt: state.timer.startedAt }); +} + +export function eventStop(state: RuntimeState) { + formattedReport = null; + if (state.eventNow === null) { + // This is normal and happens every time we call load + return; + } + + const prevReport = report.get(state.eventNow.id); + + if (prevReport === undefined) { + //we can't stop it if the is no start + return; + } + + if (prevReport.startAt === null) { + //we can't stop it if the is no start, so better to clear out bad data + report.delete(state.eventNow.id); + } + + prevReport.endAt = state.clock; +} From f4ff4d1195901d4d66d2e7d0d855733780819ddd Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 25 Jan 2025 16:05:55 +0100 Subject: [PATCH 02/20] write report from runtimeState --- apps/server/src/api-data/report/report.service.ts | 1 + apps/server/src/stores/runtimeState.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index 37459ca5df..2fa3e9a6fb 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -4,6 +4,7 @@ import { RuntimeState } from '../../stores/runtimeState.js'; //TODO: there seams to be some actions that should invalidate reports // events timer edits? // event delete +// Also what about roll mode? const report = new Map(); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 4db8dd88ac..c989f948c9 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -32,6 +32,8 @@ import { import { timerConfig } from '../config/config.js'; import { loadRoll, normaliseRollStart } from '../services/rollUtils.js'; +import * as report from '../api-data/report/report.service.js'; + const initialRuntime: Runtime = { selectedEventIndex: null, // changes if rundown changes or we load a new event numEvents: 0, // change initiated by user @@ -179,6 +181,7 @@ export function load( rundown: OntimeRundown, initialData?: Partial, ): boolean { + report.eventStop(runtimeState); // we need to persist the current block state across loads const prevCurrentBlock = { ...runtimeState.currentBlock }; clear(); @@ -417,6 +420,7 @@ export function start(state: RuntimeState = runtimeState): boolean { state.runtime.offset = getRuntimeOffset(state); state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset; + report.eventStart(runtimeState); return true; } @@ -436,6 +440,7 @@ export function stop(state: RuntimeState = runtimeState): boolean { return false; } + report.eventStop(runtimeState); clear(); runtimeState.runtime.actualStart = null; runtimeState.runtime.expectedEnd = null; From a32c10c486b6b447ea6e39b1d34c6fd4ff11655a Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 25 Jan 2025 17:00:31 +0100 Subject: [PATCH 03/20] use in UI --- apps/client/src/common/api/constants.ts | 1 + apps/client/src/common/api/report.ts | 14 +++++ .../src/common/hooks-query/useReport.ts | 19 +++++++ .../rundown/event-block/EventBlockInner.tsx | 1 + .../event-block/composite/EventBlockChip.tsx | 55 ++++++++++++++++++- apps/server/src/api-data/index.ts | 2 + .../src/api-data/report/report.controller.ts | 8 +++ .../src/api-data/report/report.router.ts | 7 +++ 8 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 apps/client/src/common/api/report.ts create mode 100644 apps/client/src/common/hooks-query/useReport.ts create mode 100644 apps/server/src/api-data/report/report.controller.ts create mode 100644 apps/server/src/api-data/report/report.router.ts diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index 98629d28b2..52f2812117 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -14,6 +14,7 @@ export const SHEET_STATE = ['sheetState']; export const URL_PRESETS = ['urlpresets']; export const VIEW_SETTINGS = ['viewSettings']; export const CLIENT_LIST = ['clientList']; +export const REPORT = ['report']; // API URLs export const apiEntryUrl = `${serverURL}/data`; diff --git a/apps/client/src/common/api/report.ts b/apps/client/src/common/api/report.ts new file mode 100644 index 0000000000..d460032572 --- /dev/null +++ b/apps/client/src/common/api/report.ts @@ -0,0 +1,14 @@ +import axios from 'axios'; +import { OntimeReport } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +export const reportUrl = `${apiEntryUrl}/report`; + +/** + * HTTP request to fetch all events + */ +export async function fetchReport(): Promise { + const res = await axios.get(`${reportUrl}/`); + return res.data; +} diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts new file mode 100644 index 0000000000..88bdb9b73e --- /dev/null +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; +import { OntimeReport } from 'ontime-types'; + +import { queryRefetchIntervalSlow } from '../../ontimeConfig'; +import { REPORT } from '../api/constants'; +import { fetchReport } from '../api/report'; + +export default function useReport() { + const { data } = useQuery({ + queryKey: REPORT, + queryFn: fetchReport, + placeholderData: (previousData, _previousQuery) => previousData, + retry: 5, + retryDelay: (attempt) => attempt * 2500, + refetchInterval: queryRefetchIntervalSlow, + networkMode: 'always', + }); + return { data: data ?? {} }; +} diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index a59450486a..b2e549edcf 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -125,6 +125,7 @@ function EventBlockInner(props: EventBlockInnerProps) { isLoaded={loaded} totalGap={totalGap} isLinkedAndNext={isNext && linkStart !== null} + duration={duration} /> )}
diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index 7a6857cbb4..dd258f57ba 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -1,8 +1,10 @@ import { useMemo } from 'react'; import { Tooltip } from '@chakra-ui/react'; +import type { OntimeReportData } from 'ontime-types'; import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket'; +import useReport from '../../../../common/hooks-query/useReport'; import { cx } from '../../../../common/utils/styleUtils'; import { formatDuration } from '../../../../common/utils/time'; import { tooltipDelayFast } from '../../../../ontimeConfig'; @@ -17,10 +19,11 @@ interface EventBlockChipProps { className: string; totalGap: number; isLinkedAndNext: boolean; + duration: number; } export default function EventBlockChip(props: EventBlockChipProps) { - const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext } = props; + const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext, id, duration } = props; const { playback } = usePlayback(); if (isLoaded) { @@ -30,7 +33,7 @@ export default function EventBlockChip(props: EventBlockChipProps) { const playbackActive = isPlaybackActive(playback); if (!playbackActive || isPast) { - return null; //TODO: Event report will go here + return ; } if (playbackActive) { @@ -75,3 +78,51 @@ function EventUntil(props: EventUntilProps) { ); } + +interface EventReportProps { + className: string; + id: string; + duration: number; +} + +function EventReport(props: EventReportProps) { + const { className, id, duration } = props; + const { data } = useReport(); + + const currentReport: OntimeReportData | undefined = data[id]; + + const [value, isOver] = useMemo(() => { + if (!currentReport) { + return [null, false]; + } + const { startAt, endAt } = currentReport; + if (!startAt || !endAt) { + return [null, false]; + } + + const actualDuration = endAt - startAt; + const difference = actualDuration - duration; + const absDifference = Math.abs(difference); + + if (absDifference < MILLIS_PER_SECOND) { + return ['==', false]; //TODO: how to symbolize ontime? + } + + const isOver = difference > 0; + + const value = `${isOver ? '+' : '-'}${formatDuration(Math.abs(difference), difference > 2 * MILLIS_PER_MINUTE)}`; + return [value, isOver]; + }, [currentReport, duration]); + + if (!value) { + return null; + } + + return ( + +
+
{value}
+
+
+ ); +} diff --git a/apps/server/src/api-data/index.ts b/apps/server/src/api-data/index.ts index d7211b9096..da5c369fac 100644 --- a/apps/server/src/api-data/index.ts +++ b/apps/server/src/api-data/index.ts @@ -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) => { diff --git a/apps/server/src/api-data/report/report.controller.ts b/apps/server/src/api-data/report/report.controller.ts new file mode 100644 index 0000000000..2bd2314221 --- /dev/null +++ b/apps/server/src/api-data/report/report.controller.ts @@ -0,0 +1,8 @@ +import type { Request, Response } from 'express'; +import type { OntimeReport } from 'ontime-types'; +import * as report from './report.service.js'; + +export async function getAll(_req: Request, res: Response) { + res.json(report.generate()); +} + diff --git a/apps/server/src/api-data/report/report.router.ts b/apps/server/src/api-data/report/report.router.ts new file mode 100644 index 0000000000..3bfbbbc36a --- /dev/null +++ b/apps/server/src/api-data/report/report.router.ts @@ -0,0 +1,7 @@ +import express from 'express'; +import { getAll } from './report.controller.js'; + +export const router = express.Router(); + +router.get('/', getAll); + From 6746f654b8b1cac9ac2fb6f9d13d9301302da140 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 25 Jan 2025 17:22:19 +0100 Subject: [PATCH 04/20] clear report --- apps/client/src/common/api/report.ts | 10 +++++++++- apps/client/src/features/rundown/RundownEntry.tsx | 12 +++++++++++- .../src/features/rundown/event-block/EventBlock.tsx | 7 +++++++ apps/server/src/api-data/report/report.controller.ts | 5 +++++ apps/server/src/api-data/report/report.router.ts | 4 +++- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/client/src/common/api/report.ts b/apps/client/src/common/api/report.ts index d460032572..b3c781c516 100644 --- a/apps/client/src/common/api/report.ts +++ b/apps/client/src/common/api/report.ts @@ -1,7 +1,9 @@ import axios from 'axios'; import { OntimeReport } from 'ontime-types'; -import { apiEntryUrl } from './constants'; +import { ontimeQueryClient } from '../../common/queryClient'; + +import { apiEntryUrl, REPORT } from './constants'; export const reportUrl = `${apiEntryUrl}/report`; @@ -9,6 +11,12 @@ export const reportUrl = `${apiEntryUrl}/report`; * HTTP request to fetch all events */ export async function fetchReport(): Promise { + console.log('fetch report') const res = await axios.get(`${reportUrl}/`); return res.data; } + +export async function deleteReport(id: string) { + await axios.delete(`${reportUrl}/${id}`); + await ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); +} diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 386950aa52..d128b683e4 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; +import { deleteReport } from '../../common/api/report'; import { useEventAction } from '../../common/hooks/useEventAction'; import useMemoisedFn from '../../common/hooks/useMemoisedFn'; import { useEmitLog } from '../../common/stores/logger'; @@ -22,7 +23,8 @@ export type EventItemActions = | 'delete' | 'clone' | 'update' - | 'swap'; + | 'swap' + | 'clear-report'; interface RundownEntryProps { type: SupportedEvent; @@ -142,6 +144,14 @@ export default function RundownEntry(props: RundownEntryProps) { return emitError(`Unknown field: ${field}`); } + case 'clear-report': { + const { field, value } = payload as FieldValue; + if (field === undefined || field !== 'id' || value === undefined || typeof value !== 'string') { + return; + } + deleteReport(value); + break; + } default: throw new Error(`Unhandled event ${action}`); } diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 8fd2fc4eaa..52f360ab7a 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -9,6 +9,7 @@ import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; +import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin'; import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink'; import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types'; @@ -177,6 +178,12 @@ export default function EventBlock(props: EventBlockProps) { }, { withDivider: true, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') }, { withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') }, + { + withDivider: true, + label: 'Clear Report', + icon: IoTrashBin, + onClick: () => actionHandler('clear-report', { field: 'id', value: eventId }), + }, ], ); diff --git a/apps/server/src/api-data/report/report.controller.ts b/apps/server/src/api-data/report/report.controller.ts index 2bd2314221..2614362359 100644 --- a/apps/server/src/api-data/report/report.controller.ts +++ b/apps/server/src/api-data/report/report.controller.ts @@ -6,3 +6,8 @@ export async function getAll(_req: Request, res: Response) { res.json(report.generate()); } +export async function deleteWithId(req: Request, res: Response) { + const { eventId } = req.params; + report.clear(eventId); + res.status(200).send(); +} diff --git a/apps/server/src/api-data/report/report.router.ts b/apps/server/src/api-data/report/report.router.ts index 3bfbbbc36a..a00fd6d30d 100644 --- a/apps/server/src/api-data/report/report.router.ts +++ b/apps/server/src/api-data/report/report.router.ts @@ -1,7 +1,9 @@ import express from 'express'; -import { getAll } from './report.controller.js'; +import { getAll, deleteWithId } from './report.controller.js'; +import { paramsMustHaveEventId } from '../rundown/rundown.validation.js'; export const router = express.Router(); router.get('/', getAll); +router.delete('/:eventId', paramsMustHaveEventId, deleteWithId); From cc7735db2959724b69a08e798191ca5e6f84960c Mon Sep 17 00:00:00 2001 From: arc-alex Date: Wed, 29 Jan 2025 23:47:54 +0100 Subject: [PATCH 05/20] update types --- .../event-block/composite/EventBlockChip.tsx | 12 ++++++------ .../src/api-data/report/report.service.ts | 18 +++++++++--------- .../types/src/definitions/core/Report.type.ts | 8 ++++++++ packages/types/src/index.ts | 3 +++ 4 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 packages/types/src/definitions/core/Report.type.ts diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index dd258f57ba..ee9b265e0d 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { Tooltip } from '@chakra-ui/react'; -import type { OntimeReportData } from 'ontime-types'; +import type { OntimeEventReport } from 'ontime-types'; import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket'; @@ -88,19 +88,19 @@ interface EventReportProps { function EventReport(props: EventReportProps) { const { className, id, duration } = props; const { data } = useReport(); - - const currentReport: OntimeReportData | undefined = data[id]; + const currentReport: OntimeEventReport | undefined = data[id]; const [value, isOver] = useMemo(() => { if (!currentReport) { return [null, false]; } - const { startAt, endAt } = currentReport; - if (!startAt || !endAt) { + + const { startedAt, endedAt } = currentReport; + if (!startedAt || !endedAt) { return [null, false]; } - const actualDuration = endAt - startAt; + const actualDuration = endedAt - startedAt; const difference = actualDuration - duration; const absDifference = Math.abs(difference); diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index 2fa3e9a6fb..a8bcb354da 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -1,4 +1,4 @@ -import { OntimeReport, OntimeReportData } from 'ontime-types'; +import { OntimeReport, OntimeEventReport } from 'ontime-types'; import { RuntimeState } from '../../stores/runtimeState.js'; //TODO: there seams to be some actions that should invalidate reports @@ -6,16 +6,16 @@ import { RuntimeState } from '../../stores/runtimeState.js'; // event delete // Also what about roll mode? -const report = new Map(); +const report = new Map(); let formattedReport: OntimeReport | null = null; /** * blank placeholder data */ -const blankReportData: OntimeReportData = { - startAt: null, - endAt: null, +const blankReportData: OntimeEventReport = { + startedAt: null, + endedAt: null, } as const; /** @@ -29,7 +29,7 @@ export function generate(): OntimeReport { return formattedReport; } -export function getWithId(id: string): OntimeReportData | null { +export function getWithId(id: string): OntimeEventReport | null { return report.get(id) ?? null; } @@ -57,7 +57,7 @@ export function eventStart(state: RuntimeState) { } // this clears out potentaly old data - report.set(state.eventNow.id, { ...blankReportData, startAt: state.timer.startedAt }); + report.set(state.eventNow.id, { ...blankReportData, startedAt: state.timer.startedAt }); } export function eventStop(state: RuntimeState) { @@ -74,10 +74,10 @@ export function eventStop(state: RuntimeState) { return; } - if (prevReport.startAt === null) { + if (prevReport.startedAt === null) { //we can't stop it if the is no start, so better to clear out bad data report.delete(state.eventNow.id); } - prevReport.endAt = state.clock; + prevReport.endedAt = state.clock; } diff --git a/packages/types/src/definitions/core/Report.type.ts b/packages/types/src/definitions/core/Report.type.ts new file mode 100644 index 0000000000..c34a0978d8 --- /dev/null +++ b/packages/types/src/definitions/core/Report.type.ts @@ -0,0 +1,8 @@ +import type { MaybeNumber } from '../../utils/utils.type.js'; + +export type OntimeEventReport = { + startedAt: MaybeNumber; + endedAt: MaybeNumber; +}; + +export type OntimeReport = Record; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 396c135942..f33c3a0437 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -16,6 +16,9 @@ export type { OntimeEntryCommonKeys, OntimeRundown, OntimeRundownEntry } from '. export { TimeStrategy } from './definitions/TimeStrategy.type.js'; export { TimerType } from './definitions/TimerType.type.js'; +// ---> Report +export type { OntimeReport, OntimeEventReport } from './definitions/core/Report.type.js'; + // ---> Automations export type { Automation, From c4440bc6e11f5b23a3e422009ecf07812fcbb2ac Mon Sep 17 00:00:00 2001 From: arc-alex Date: Thu, 30 Jan 2025 21:25:52 +0100 Subject: [PATCH 06/20] clear all from settings menu --- apps/client/src/common/api/report.ts | 6 ++- .../src/common/hooks-query/useReport.ts | 4 +- .../FeatureSettingsPanel.tsx | 6 +++ .../feature-settings-panel/ReportSettings.tsx | 46 +++++++++++++++++++ .../app-settings/useAppSettingsMenu.tsx | 1 + .../src/api-data/report/report.controller.ts | 5 ++ .../src/api-data/report/report.router.ts | 3 +- .../src/api-data/report/report.service.ts | 1 + 8 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx diff --git a/apps/client/src/common/api/report.ts b/apps/client/src/common/api/report.ts index b3c781c516..e4d42484a5 100644 --- a/apps/client/src/common/api/report.ts +++ b/apps/client/src/common/api/report.ts @@ -11,7 +11,6 @@ export const reportUrl = `${apiEntryUrl}/report`; * HTTP request to fetch all events */ export async function fetchReport(): Promise { - console.log('fetch report') const res = await axios.get(`${reportUrl}/`); return res.data; } @@ -20,3 +19,8 @@ export async function deleteReport(id: string) { await axios.delete(`${reportUrl}/${id}`); await ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); } + +export async function deleteAllReport() { + await axios.delete(`${reportUrl}/all`); + await ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); +} diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index 88bdb9b73e..cec5d79674 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -6,7 +6,7 @@ import { REPORT } from '../api/constants'; import { fetchReport } from '../api/report'; export default function useReport() { - const { data } = useQuery({ + const { data, status } = useQuery({ queryKey: REPORT, queryFn: fetchReport, placeholderData: (previousData, _previousQuery) => previousData, @@ -15,5 +15,5 @@ export default function useReport() { refetchInterval: queryRefetchIntervalSlow, networkMode: 'always', }); - return { data: data ?? {} }; + return { data: data ?? {}, status }; } diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx index e126011670..b636af1d72 100644 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx @@ -3,11 +3,13 @@ import type { PanelBaseProps } from '../../panel-list/PanelList'; import * as Panel from '../../panel-utils/PanelUtils'; import CustomFields from './custom-fields/CustomFields'; +import ReportSettings from './ReportSettings'; import UrlPresetsForm from './UrlPresetsForm'; export default function FeatureSettingsPanel({ location }: PanelBaseProps) { const customFieldsRef = useScrollIntoView('custom', location); const urlPresetsRef = useScrollIntoView('urlpresets', location); + const reportRef = useScrollIntoView('report', location); return ( <> @@ -19,6 +21,10 @@ export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
+ +
+ +
); } diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx new file mode 100644 index 0000000000..f21839be6e --- /dev/null +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx @@ -0,0 +1,46 @@ +import { useCallback } from 'react'; +import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react'; +import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin'; + +import { deleteAllReport } from '../../../../common/api/report'; +import ExternalLink from '../../../../common/components/external-link/ExternalLink'; +import useReport from '../../../../common/hooks-query/useReport'; +import * as Panel from '../../panel-utils/PanelUtils'; + +const urlReport = 'https://docs.getontime.no/ontime/report'; + +export default function ReportSettings() { + const { status } = useReport(); + + const isLoading = status === 'pending'; + + const clear = useCallback(async () => { + console.log('cler') + await deleteAllReport(); + }, []); + + return ( + + + Report + + + + + TODO: Explain something about report here + See the docs + + + + + + Manage reports + + + + + + ); +} diff --git a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx index 6acaa1b9b0..7dc41eecc7 100644 --- a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx +++ b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx @@ -35,6 +35,7 @@ const staticOptions = [ secondary: [ { id: 'feature_settings__custom', label: 'Custom fields' }, { id: 'feature_settings__urlpresets', label: 'URL Presets' }, + { id: 'feature_settings__report', label: 'Reporter' }, ], }, { diff --git a/apps/server/src/api-data/report/report.controller.ts b/apps/server/src/api-data/report/report.controller.ts index 2614362359..e0251baf97 100644 --- a/apps/server/src/api-data/report/report.controller.ts +++ b/apps/server/src/api-data/report/report.controller.ts @@ -6,6 +6,11 @@ export async function getAll(_req: Request, res: Response) { res.json(report.generate()); } +export async function deleteAll(_req: Request, res: Response) { + report.clear(); + res.status(200).send(); +} + export async function deleteWithId(req: Request, res: Response) { const { eventId } = req.params; report.clear(eventId); diff --git a/apps/server/src/api-data/report/report.router.ts b/apps/server/src/api-data/report/report.router.ts index a00fd6d30d..cb7fb622e5 100644 --- a/apps/server/src/api-data/report/report.router.ts +++ b/apps/server/src/api-data/report/report.router.ts @@ -1,9 +1,10 @@ import express from 'express'; -import { getAll, deleteWithId } from './report.controller.js'; +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); diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index a8bcb354da..0bca0c2325 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -38,6 +38,7 @@ export function getWithId(id: string): OntimeEventReport | null { * @param id optional id of a event report to clear */ export function clear(id?: string) { + console.log('clear report',id) formattedReport = null; if (id) { report.delete(id); From a35255ca15154e31e482d0560f02aeccf7fa5b92 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 1 Feb 2025 16:26:23 +0100 Subject: [PATCH 07/20] rearence rightclik menu --- .../src/features/rundown/event-block/EventBlock.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 52f360ab7a..bb25d6695d 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -2,6 +2,7 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; +import { IoCloseCircle } from '@react-icons/all-files/io5/IoCloseCircle'; import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline'; import { IoLink } from '@react-icons/all-files/io5/IoLink'; import { IoPeople } from '@react-icons/all-files/io5/IoPeople'; @@ -9,7 +10,6 @@ import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin'; import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink'; import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types'; @@ -176,14 +176,14 @@ export default function EventBlock(props: EventBlockProps) { }, isDisabled: selectedEventId == null || selectedEventId === eventId, }, - { withDivider: true, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') }, - { withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') }, + { withDivider: false, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') }, { withDivider: true, - label: 'Clear Report', - icon: IoTrashBin, + label: 'Clear report', + icon: IoCloseCircle, onClick: () => actionHandler('clear-report', { field: 'id', value: eventId }), }, + { withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') }, ], ); From 9ce972f28948f0a3fa56e36ec964edd648457ac2 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 1 Feb 2025 17:10:11 +0100 Subject: [PATCH 08/20] refactor styling --- .../composite/EventBlockChip.module.scss | 8 ++++-- .../event-block/composite/EventBlockChip.tsx | 25 +++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss index b696964318..6d49837405 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss @@ -6,14 +6,18 @@ color: $label-gray; padding: 0.125rem 0.5rem; border-radius: 2px; - + &.over { color: $playback-negative; } - + &.under { color: $playback-ahead; } + + &.ontime { + color: $ontime-color; + } &.due { color: $warning-orange; diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index ee9b265e0d..7f0f8d2e06 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -1,12 +1,13 @@ import { useMemo } from 'react'; import { Tooltip } from '@chakra-ui/react'; +import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle'; import type { OntimeEventReport } from 'ontime-types'; import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket'; import useReport from '../../../../common/hooks-query/useReport'; import { cx } from '../../../../common/utils/styleUtils'; -import { formatDuration } from '../../../../common/utils/time'; +import { formatDuration, formatTime } from '../../../../common/utils/time'; import { tooltipDelayFast } from '../../../../ontimeConfig'; import style from './EventBlockChip.module.scss'; @@ -90,14 +91,14 @@ function EventReport(props: EventReportProps) { const { data } = useReport(); const currentReport: OntimeEventReport | undefined = data[id]; - const [value, isOver] = useMemo(() => { + const [value, overUnderStyle, tooltip] = useMemo(() => { if (!currentReport) { - return [null, false]; + return [null, 'none', '']; } const { startedAt, endedAt } = currentReport; if (!startedAt || !endedAt) { - return [null, false]; + return [null, 'none', '']; } const actualDuration = endedAt - startedAt; @@ -105,13 +106,17 @@ function EventReport(props: EventReportProps) { const absDifference = Math.abs(difference); if (absDifference < MILLIS_PER_SECOND) { - return ['==', false]; //TODO: how to symbolize ontime? + return ['ontime', 'ontime', 'Event finished ontime']; } const isOver = difference > 0; - const value = `${isOver ? '+' : '-'}${formatDuration(Math.abs(difference), difference > 2 * MILLIS_PER_MINUTE)}`; - return [value, isOver]; + const fullTimeValue = formatTime(absDifference); + + const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`; + + const value = `${isOver ? '+' : '-'}${formatDuration(absDifference, absDifference > 2 * MILLIS_PER_MINUTE)}`; + return [value, isOver ? 'over' : 'under', tooltip]; }, [currentReport, duration]); if (!value) { @@ -119,9 +124,9 @@ function EventReport(props: EventReportProps) { } return ( - -
-
{value}
+ +
+ {value === 'ontime' ? : value}
); From 619ef3ca0d548bbec619c62e67706de9ce53377a Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 1 Feb 2025 17:16:43 +0100 Subject: [PATCH 09/20] also report roll events --- apps/server/src/api-data/report/report.service.ts | 5 ++--- apps/server/src/stores/runtimeState.ts | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index 0bca0c2325..b2dd38c5c2 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -38,7 +38,6 @@ export function getWithId(id: string): OntimeEventReport | null { * @param id optional id of a event report to clear */ export function clear(id?: string) { - console.log('clear report',id) formattedReport = null; if (id) { report.delete(id); @@ -47,7 +46,7 @@ export function clear(id?: string) { } } -export function eventStart(state: RuntimeState) { +export function eventStart(state: Readonly) { formattedReport = null; if (state.eventNow === null) { // eslint-disable-next-line no-unused-labels -- dev code path @@ -61,7 +60,7 @@ export function eventStart(state: RuntimeState) { report.set(state.eventNow.id, { ...blankReportData, startedAt: state.timer.startedAt }); } -export function eventStop(state: RuntimeState) { +export function eventStop(state: Readonly) { formattedReport = null; if (state.eventNow === null) { // This is normal and happens every time we call load diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index c989f948c9..a46582cf2f 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -620,6 +620,7 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString runtimeState.runtime.actualStart = runtimeState.clock; } runtimeState.timer.secondaryTimer = null; + report.eventStart(runtimeState); } else { runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock); runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; @@ -698,6 +699,7 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString // update runtime runtimeState.runtime.actualStart = runtimeState.clock; + report.eventStart(runtimeState); return { eventId: runtimeState.eventNow.id, didStart: true }; } From 72dfd2b0fd777cc36bf56bc0a1a6fd0a3e702511 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sun, 2 Feb 2025 15:14:30 +0100 Subject: [PATCH 10/20] ontime/under time is same colour --- apps/client/src/features/overview/Overview.module.scss | 2 +- .../event-block/composite/EventBlockChip.module.scss | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/client/src/features/overview/Overview.module.scss b/apps/client/src/features/overview/Overview.module.scss index 92847e298a..e178d95f52 100644 --- a/apps/client/src/features/overview/Overview.module.scss +++ b/apps/client/src/features/overview/Overview.module.scss @@ -46,7 +46,7 @@ } .ahead { - color: $green-500; + color: $playback-ahead; } .behind { diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss index 6d49837405..d59e427369 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss @@ -6,18 +6,14 @@ color: $label-gray; padding: 0.125rem 0.5rem; border-radius: 2px; - + &.over { - color: $playback-negative; + color: $ontime-delay-text; } - + &.under { color: $playback-ahead; } - - &.ontime { - color: $ontime-color; - } &.due { color: $warning-orange; From 19ace9f260dc336e7b77ee146654c910773d42cf Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sun, 2 Feb 2025 16:00:47 +0100 Subject: [PATCH 11/20] refactor reporter --- .../src/api-data/report/report.service.ts | 58 ++++++++----------- .../runtime-service/RuntimeService.ts | 10 +++- apps/server/src/stores/runtimeState.ts | 9 --- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index b2dd38c5c2..e9d7cbe5d2 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -1,4 +1,4 @@ -import { OntimeReport, OntimeEventReport } from 'ontime-types'; +import { OntimeReport, OntimeEventReport, TimerLifeCycle, MaybeString } from 'ontime-types'; import { RuntimeState } from '../../stores/runtimeState.js'; //TODO: there seams to be some actions that should invalidate reports @@ -46,38 +46,30 @@ export function clear(id?: string) { } } -export function eventStart(state: Readonly) { - formattedReport = null; - if (state.eventNow === null) { - // eslint-disable-next-line no-unused-labels -- dev code path - DEV: { - throw new Error('report.eventStart: called without eventNow present'); - } - return; - } - - // this clears out potentaly old data - report.set(state.eventNow.id, { ...blankReportData, startedAt: state.timer.startedAt }); -} - -export function eventStop(state: Readonly) { - formattedReport = null; - if (state.eventNow === null) { - // This is normal and happens every time we call load - return; - } +let currentReportId: MaybeString = null; - const prevReport = report.get(state.eventNow.id); - - if (prevReport === undefined) { - //we can't stop it if the is no start - return; - } - - if (prevReport.startedAt === null) { - //we can't stop it if the is no start, so better to clear out bad data - report.delete(state.eventNow.id); +/** + * trigger report entry + * @param cycle + * @param state + * @returns + */ +export function triggerReportEntry(cycle: TimerLifeCycle, state: Readonly) { + switch (cycle) { + case TimerLifeCycle.onStart: { + currentReportId = state.eventNow.id; + report.set(currentReportId, { ...blankReportData, startedAt: state.timer.startedAt }); + break; + } + case TimerLifeCycle.onLoad: + case TimerLifeCycle.onStop: { + if (currentReportId) { + const startedAt = report.get(currentReportId).startedAt; + report.set(currentReportId, { startedAt, endedAt: state.clock }); + currentReportId = null; + formattedReport = null; + } + break; + } } - - prevReport.endedAt = state.clock; } diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 2d6fe844ff..ba179de11d 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -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 { @@ -294,8 +296,10 @@ class RuntimeService { 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.onLoad, newState); + triggerAutomations(TimerLifeCycle.onLoad, newState); }); } return success; @@ -474,6 +478,7 @@ class RuntimeService { if (didStart) { process.nextTick(() => { + triggerReportEntry(TimerLifeCycle.onStart, newState); triggerAutomations(TimerLifeCycle.onStart, newState); }); } @@ -546,6 +551,7 @@ class RuntimeService { const newState = runtimeState.getState(); logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`); process.nextTick(() => { + triggerReportEntry(TimerLifeCycle.onStop, newState); triggerAutomations(TimerLifeCycle.onStop, newState); }); @@ -598,12 +604,14 @@ class RuntimeService { if (result.eventId !== previousState.eventNow?.id) { logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`); process.nextTick(() => { + triggerReportEntry(TimerLifeCycle.onLoad, newState); triggerAutomations(TimerLifeCycle.onLoad, newState); }); } if (result.didStart) { process.nextTick(() => { + triggerReportEntry(TimerLifeCycle.onStart, newState); triggerAutomations(TimerLifeCycle.onStart, newState); }); } diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index a46582cf2f..6724ec9e20 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -32,8 +32,6 @@ import { import { timerConfig } from '../config/config.js'; import { loadRoll, normaliseRollStart } from '../services/rollUtils.js'; -import * as report from '../api-data/report/report.service.js'; - const initialRuntime: Runtime = { selectedEventIndex: null, // changes if rundown changes or we load a new event numEvents: 0, // change initiated by user @@ -181,7 +179,6 @@ export function load( rundown: OntimeRundown, initialData?: Partial, ): boolean { - report.eventStop(runtimeState); // we need to persist the current block state across loads const prevCurrentBlock = { ...runtimeState.currentBlock }; clear(); @@ -419,8 +416,6 @@ export function start(state: RuntimeState = runtimeState): boolean { // update offset state.runtime.offset = getRuntimeOffset(state); state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset; - - report.eventStart(runtimeState); return true; } @@ -439,8 +434,6 @@ export function stop(state: RuntimeState = runtimeState): boolean { if (state.timer.playback === Playback.Stop) { return false; } - - report.eventStop(runtimeState); clear(); runtimeState.runtime.actualStart = null; runtimeState.runtime.expectedEnd = null; @@ -620,7 +613,6 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString runtimeState.runtime.actualStart = runtimeState.clock; } runtimeState.timer.secondaryTimer = null; - report.eventStart(runtimeState); } else { runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock); runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock; @@ -699,7 +691,6 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString // update runtime runtimeState.runtime.actualStart = runtimeState.clock; - report.eventStart(runtimeState); return { eventId: runtimeState.eventNow.id, didStart: true }; } From 0844db187034965e73e19fdf22d14a88d439eb52 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sun, 2 Feb 2025 16:22:37 +0100 Subject: [PATCH 12/20] add target to ontime-refetch --- .../src/common/hooks-query/useReport.ts | 3 +-- apps/client/src/common/utils/socket.ts | 20 +++++++++++-------- .../src/api-data/report/report.service.ts | 4 ++++ .../rundown-service/RundownService.ts | 1 + 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index cec5d79674..089dae6da0 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { OntimeReport } from 'ontime-types'; -import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { REPORT } from '../api/constants'; import { fetchReport } from '../api/report'; @@ -12,8 +11,8 @@ export default function useReport() { placeholderData: (previousData, _previousQuery) => previousData, retry: 5, retryDelay: (attempt) => attempt * 2500, - refetchInterval: queryRefetchIntervalSlow, networkMode: 'always', + enabled: false, }); return { data: data ?? {}, status }; } diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index f61360122e..d7cf799586 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -1,7 +1,7 @@ import { Log, RundownCached, RuntimeStore } from 'ontime-types'; import { isProduction, websocketUrl } from '../../externals'; -import { CLIENT_LIST, CUSTOM_FIELDS, RUNDOWN, RUNTIME } from '../api/constants'; +import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants'; import { invalidateAllCaches } from '../api/utils'; import { ontimeQueryClient } from '../queryClient'; import { @@ -196,19 +196,23 @@ export const connectSocket = () => { } case 'ontime-refetch': { // the refetch message signals that the rundown has changed in the server side - const { revision, reload } = payload; - const currentRevision = ontimeQueryClient.getQueryData(RUNDOWN)?.revision ?? -1; - + const { reload, target } = payload; if (reload) { invalidateAllCaches(); - } else if (revision > currentRevision) { - ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN }); - ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); + } else if (target === 'RUNDOWN') { + const { revision } = payload; + const currentRevision = ontimeQueryClient.getQueryData(RUNDOWN)?.revision ?? -1; + if (revision > currentRevision) { + ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN }); + ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); + } + } else if (target === 'REPORT') { + ontimeQueryClient.fetchQuery({ queryKey: REPORT }); } break; } case 'ontime-flush': { - flushBatchUpdates() + flushBatchUpdates(); break; } } diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index e9d7cbe5d2..79ef3cc104 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -1,5 +1,6 @@ import { OntimeReport, OntimeEventReport, TimerLifeCycle, MaybeString } from 'ontime-types'; import { RuntimeState } from '../../stores/runtimeState.js'; +import { sendRefetch } from '../../adapters/websocketAux.js'; //TODO: there seams to be some actions that should invalidate reports // events timer edits? @@ -68,6 +69,9 @@ export function triggerReportEntry(cycle: TimerLifeCycle, state: Readonly Date: Mon, 3 Feb 2025 12:35:39 +0100 Subject: [PATCH 13/20] remove menu --- .../FeatureSettingsPanel.tsx | 6 --- .../feature-settings-panel/ReportSettings.tsx | 46 ------------------- 2 files changed, 52 deletions(-) delete mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx index b636af1d72..e126011670 100644 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx @@ -3,13 +3,11 @@ import type { PanelBaseProps } from '../../panel-list/PanelList'; import * as Panel from '../../panel-utils/PanelUtils'; import CustomFields from './custom-fields/CustomFields'; -import ReportSettings from './ReportSettings'; import UrlPresetsForm from './UrlPresetsForm'; export default function FeatureSettingsPanel({ location }: PanelBaseProps) { const customFieldsRef = useScrollIntoView('custom', location); const urlPresetsRef = useScrollIntoView('urlpresets', location); - const reportRef = useScrollIntoView('report', location); return ( <> @@ -21,10 +19,6 @@ export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
- -
- -
); } diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx deleted file mode 100644 index f21839be6e..0000000000 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/ReportSettings.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useCallback } from 'react'; -import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react'; -import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin'; - -import { deleteAllReport } from '../../../../common/api/report'; -import ExternalLink from '../../../../common/components/external-link/ExternalLink'; -import useReport from '../../../../common/hooks-query/useReport'; -import * as Panel from '../../panel-utils/PanelUtils'; - -const urlReport = 'https://docs.getontime.no/ontime/report'; - -export default function ReportSettings() { - const { status } = useReport(); - - const isLoading = status === 'pending'; - - const clear = useCallback(async () => { - console.log('cler') - await deleteAllReport(); - }, []); - - return ( - - - Report - - - - - TODO: Explain something about report here - See the docs - - - - - - Manage reports - - - - - - ); -} From ce0dc7007ffecc34b2ff407d3f0539ef2c28b0a9 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 13:21:15 +0100 Subject: [PATCH 14/20] fectch only on message from server --- apps/client/src/common/hooks-query/useReport.ts | 8 +++++--- apps/client/src/common/utils/socket.ts | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index 089dae6da0..57083ddbdd 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -5,14 +5,16 @@ import { REPORT } from '../api/constants'; import { fetchReport } from '../api/report'; export default function useReport() { - const { data, status } = useQuery({ + const { data } = useQuery({ queryKey: REPORT, queryFn: fetchReport, placeholderData: (previousData, _previousQuery) => previousData, retry: 5, retryDelay: (attempt) => attempt * 2500, networkMode: 'always', - enabled: false, + refetchOnMount: false, + enabled: true, }); - return { data: data ?? {}, status }; + + return { data: data ?? {} }; } diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index d7cf799586..c84b0fa28c 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -207,7 +207,8 @@ export const connectSocket = () => { ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); } } else if (target === 'REPORT') { - ontimeQueryClient.fetchQuery({ queryKey: REPORT }); + ontimeQueryClient.refetchQueries({ queryKey: REPORT }); + //we need to use refech and not invalidate } break; } From 3ee3794bf187c05dd0d118367645044abef6b940 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 13:59:07 +0100 Subject: [PATCH 15/20] memo useGetEventReport --- apps/client/src/common/hooks-query/useReport.ts | 8 ++++++++ .../rundown/event-block/composite/EventBlockChip.tsx | 6 ++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index 57083ddbdd..5f9da7643c 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { OntimeReport } from 'ontime-types'; @@ -18,3 +19,10 @@ export default function useReport() { return { data: data ?? {} }; } + +export function useGetEventReport(id: string) { + const { data } = useReport(); + return useMemo(() => { + return data[id]; + }, [data, id]); +} diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index 7f0f8d2e06..900c39832a 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -1,11 +1,10 @@ import { useMemo } from 'react'; import { Tooltip } from '@chakra-ui/react'; import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle'; -import type { OntimeEventReport } from 'ontime-types'; import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket'; -import useReport from '../../../../common/hooks-query/useReport'; +import { useGetEventReport } from '../../../../common/hooks-query/useReport'; import { cx } from '../../../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../../../common/utils/time'; import { tooltipDelayFast } from '../../../../ontimeConfig'; @@ -88,8 +87,7 @@ interface EventReportProps { function EventReport(props: EventReportProps) { const { className, id, duration } = props; - const { data } = useReport(); - const currentReport: OntimeEventReport | undefined = data[id]; + const currentReport = useGetEventReport(id); const [value, overUnderStyle, tooltip] = useMemo(() => { if (!currentReport) { From 9188e3b3076e6fcb9935ee42c25f5024b7b73cd9 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 15:32:15 +0100 Subject: [PATCH 16/20] refactor --- .../src/api-data/report/report.service.ts | 42 ++++++++++--------- .../runtime-service/RuntimeService.ts | 5 ++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/apps/server/src/api-data/report/report.service.ts b/apps/server/src/api-data/report/report.service.ts index 79ef3cc104..aaa25e780f 100644 --- a/apps/server/src/api-data/report/report.service.ts +++ b/apps/server/src/api-data/report/report.service.ts @@ -1,11 +1,7 @@ -import { OntimeReport, OntimeEventReport, TimerLifeCycle, MaybeString } from 'ontime-types'; +import { OntimeReport, OntimeEventReport, TimerLifeCycle } from 'ontime-types'; import { RuntimeState } from '../../stores/runtimeState.js'; import { sendRefetch } from '../../adapters/websocketAux.js'; - -//TODO: there seams to be some actions that should invalidate reports -// events timer edits? -// event delete -// Also what about roll mode? +import { DeepReadonly } from 'ts-essentials'; const report = new Map(); @@ -47,31 +43,37 @@ export function clear(id?: string) { } } -let currentReportId: MaybeString = null; - /** * trigger report entry * @param cycle * @param state * @returns */ -export function triggerReportEntry(cycle: TimerLifeCycle, state: Readonly) { +export function triggerReportEntry( + cycle: TimerLifeCycle.onStart | TimerLifeCycle.onStop, + state: DeepReadonly, +) { switch (cycle) { case TimerLifeCycle.onStart: { - currentReportId = state.eventNow.id; - report.set(currentReportId, { ...blankReportData, startedAt: state.timer.startedAt }); + report.set(state.eventNow.id, { ...blankReportData, startedAt: state.timer.startedAt }); break; } - case TimerLifeCycle.onLoad: case TimerLifeCycle.onStop: { - if (currentReportId) { - const startedAt = report.get(currentReportId).startedAt; - report.set(currentReportId, { startedAt, endedAt: state.clock }); - currentReportId = null; - formattedReport = null; - sendRefetch({ - target: 'REPORT', - }); + const activeReport = report.get(state.eventNow?.id); + // check that there is an active report for this id + if (activeReport) { + const { startedAt, endedAt } = activeReport; + // and that the correct values are populated/free + if (startedAt !== null && endedAt === null) { + report.set(state.eventNow.id, { startedAt, endedAt: state.clock }); + formattedReport = null; + sendRefetch({ + target: 'REPORT', + }); + } else { + // otherwise something is wrong and we should clear the report + report.delete(state.eventNow?.id); + } } break; } diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index ba179de11d..f50d55fea1 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -290,6 +290,7 @@ 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); @@ -298,7 +299,7 @@ class RuntimeService { logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`); const newState = runtimeState.getState(); process.nextTick(() => { - triggerReportEntry(TimerLifeCycle.onLoad, newState); + triggerReportEntry(TimerLifeCycle.onStop, previousState); triggerAutomations(TimerLifeCycle.onLoad, newState); }); } @@ -604,7 +605,7 @@ class RuntimeService { if (result.eventId !== previousState.eventNow?.id) { logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`); process.nextTick(() => { - triggerReportEntry(TimerLifeCycle.onLoad, newState); + triggerReportEntry(TimerLifeCycle.onStop, previousState); triggerAutomations(TimerLifeCycle.onLoad, newState); }); } From 82b82efecaea497a65931924d00f2605532368d0 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 17:38:23 +0100 Subject: [PATCH 17/20] use staleTime --- apps/client/src/common/hooks-query/useReport.ts | 4 ++-- apps/client/src/common/utils/socket.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index 5f9da7643c..3bab24f632 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { OntimeReport } from 'ontime-types'; +import { MILLIS_PER_HOUR } from 'ontime-utils'; import { REPORT } from '../api/constants'; import { fetchReport } from '../api/report'; @@ -13,8 +14,7 @@ export default function useReport() { retry: 5, retryDelay: (attempt) => attempt * 2500, networkMode: 'always', - refetchOnMount: false, - enabled: true, + staleTime: MILLIS_PER_HOUR, }); return { data: data ?? {} }; diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index c84b0fa28c..a79a6a020a 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -207,8 +207,7 @@ export const connectSocket = () => { ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); } } else if (target === 'REPORT') { - ontimeQueryClient.refetchQueries({ queryKey: REPORT }); - //we need to use refech and not invalidate + ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); } break; } From 4c1738c221b547cb6d2f9cc75874daa533f04b8f Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 17:56:45 +0100 Subject: [PATCH 18/20] islate tooltip for TimeUntil --- .../event-block/composite/EventBlockChip.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index 900c39832a..ca3f63e496 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -39,12 +39,11 @@ export default function EventBlockChip(props: EventBlockChipProps) { if (playbackActive) { // we extracted the component to avoid unnecessary calculations and re-renders return ( - + +
+ +
+
); } @@ -52,14 +51,13 @@ export default function EventBlockChip(props: EventBlockChipProps) { } interface EventUntilProps { - className: string; trueTimeStart: number; totalGap: number; isLinkedAndNext: boolean; } function EventUntil(props: EventUntilProps) { - const { trueTimeStart, className, totalGap, isLinkedAndNext } = props; + const { trueTimeStart, totalGap, isLinkedAndNext } = props; const { clock, offset } = useTimelineStatus(); const [timeUntilString, isDue] = useMemo(() => { @@ -71,11 +69,9 @@ function EventUntil(props: EventUntilProps) { }, [totalGap, isLinkedAndNext, offset, trueTimeStart, clock]); return ( - -
-
{timeUntilString}
-
-
+
+
{timeUntilString}
+
); } From 05d661e1507e794f4efa5416c834798d89fc0482 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 18:01:36 +0100 Subject: [PATCH 19/20] remove unneeded memo --- .../rundown/event-block/composite/EventBlockChip.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx index ca3f63e496..177082ef63 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -60,13 +60,11 @@ function EventUntil(props: EventUntilProps) { const { trueTimeStart, totalGap, isLinkedAndNext } = props; const { clock, offset } = useTimelineStatus(); - const [timeUntilString, isDue] = useMemo(() => { - const consumedOffset = isLinkedAndNext ? offset : Math.min(offset + totalGap, 0); - const offsetTimestart = trueTimeStart - consumedOffset; - const timeUntil = offsetTimestart - clock; - const isDue = timeUntil < MILLIS_PER_SECOND; - return [isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`, isDue]; - }, [totalGap, isLinkedAndNext, offset, trueTimeStart, clock]); + const consumedOffset = isLinkedAndNext ? offset : Math.min(offset + totalGap, 0); + const offsetTimestart = trueTimeStart - consumedOffset; + const timeUntil = offsetTimestart - clock; + const isDue = timeUntil < MILLIS_PER_SECOND; + const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`; return (
From 3580d42f53d537ea1b538a5726245ef7359aad7e Mon Sep 17 00:00:00 2001 From: arc-alex Date: Mon, 3 Feb 2025 18:02:46 +0100 Subject: [PATCH 20/20] dont add to menu yet --- apps/client/src/features/app-settings/useAppSettingsMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx index 7dc41eecc7..6acaa1b9b0 100644 --- a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx +++ b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx @@ -35,7 +35,6 @@ const staticOptions = [ secondary: [ { id: 'feature_settings__custom', label: 'Custom fields' }, { id: 'feature_settings__urlpresets', label: 'URL Presets' }, - { id: 'feature_settings__report', label: 'Reporter' }, ], }, {