mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-02 12:59:09 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e2baf1fa1 | |||
| 46e4ac94f1 | |||
| b5e1bccc70 | |||
| c840add9c5 |
@@ -15,6 +15,7 @@ export const RUNDOWN = ['rundown'];
|
|||||||
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
|
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
|
||||||
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
|
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
export const SESSION_STATS = ['sessionStats'];
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const CSS_OVERRIDE = ['cssOverride'];
|
export const CSS_OVERRIDE = ['cssOverride'];
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { GetInfo, LinkOptions } from 'ontime-types';
|
import { GetInfo, LinkOptions, SessionStats } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const sessionPath = `${apiEntryUrl}/session`;
|
const sessionPath = `${apiEntryUrl}/session`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve statistics of the running session
|
||||||
|
*/
|
||||||
|
export async function getSessionStats(options?: RequestOptions): Promise<SessionStats> {
|
||||||
|
const res = await axios.get(sessionPath, { signal: options?.signal });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application info
|
* HTTP request to retrieve application info
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import Select from '../select/Select';
|
|||||||
|
|
||||||
import style from './RedirectClientModal.module.scss';
|
import style from './RedirectClientModal.module.scss';
|
||||||
|
|
||||||
|
/** the first navigator view, used as the initial value of the view select */
|
||||||
|
const defaultView = `/${navigatorConstants[0].url}`;
|
||||||
|
|
||||||
interface RedirectClientModalProps {
|
interface RedirectClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -25,7 +28,7 @@ interface RedirectClientModalProps {
|
|||||||
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
||||||
const { data } = useUrlPresets();
|
const { data } = useUrlPresets();
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
const [selected, setSelected] = useState('/');
|
const [selected, setSelected] = useState(defaultView);
|
||||||
|
|
||||||
const { setRedirect } = setClientRemote;
|
const { setRedirect } = setClientRemote;
|
||||||
|
|
||||||
@@ -97,19 +100,18 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
<Select
|
<Select
|
||||||
fluid
|
fluid
|
||||||
options={viewOptions}
|
options={viewOptions}
|
||||||
defaultValue={viewOptions[0].value}
|
defaultValue={defaultView}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === null) return;
|
if (value === null) return;
|
||||||
setSelected(value);
|
setSelected(value);
|
||||||
}}
|
}}
|
||||||
disabled={enabledPresets.length === 0}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
aria-label='Redirect to preset'
|
aria-label='Redirect to preset'
|
||||||
className={style.redirect}
|
className={style.redirect}
|
||||||
disabled={enabledPresets.length === 0 || selected === '/'}
|
disabled={selected === currentPath}
|
||||||
onClick={() => handleRedirect(selected)}
|
onClick={() => handleRedirect(selected)}
|
||||||
>
|
>
|
||||||
Redirect <IoArrowForward />
|
Redirect <IoArrowForward />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
import Button from '../../../common/components/buttons/Button';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
import { isKeyEnter } from '../../utils/keyEvent';
|
||||||
import Dialog from '../dialog/Dialog';
|
import Dialog from '../dialog/Dialog';
|
||||||
import Input from '../input/input/Input';
|
import Input from '../input/input/Input';
|
||||||
|
|
||||||
@@ -33,7 +34,18 @@ export function RenameClientModal({ id, name: currentName = '', isOpen, onClose
|
|||||||
showCloseButton
|
showCloseButton
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
bodyElements={
|
bodyElements={
|
||||||
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
|
<Input
|
||||||
|
height='large'
|
||||||
|
placeholder='New name'
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (isKeyEnter(event) && canSubmit) {
|
||||||
|
handleRename();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
footerElements={
|
footerElements={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { SessionStats } from 'ontime-types';
|
||||||
|
|
||||||
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
|
import { SESSION_STATS } from '../api/constants';
|
||||||
|
import { getSessionStats } from '../api/session';
|
||||||
|
|
||||||
|
export default function useSessionStats() {
|
||||||
|
const { data, status, isError, refetch } = useQuery<SessionStats>({
|
||||||
|
queryKey: SESSION_STATS,
|
||||||
|
queryFn: ({ signal }) => getSessionStats({ signal }),
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
refetchInterval: queryRefetchInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, status, isError, refetch };
|
||||||
|
}
|
||||||
@@ -15,11 +15,14 @@ const logger = createStore<LogStore>(() => ({
|
|||||||
logs: [],
|
logs: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/** the log is kept in memory, we cap it to avoid it growing through a long show */
|
||||||
|
const maxLogEntries = 500;
|
||||||
|
|
||||||
export const useLogData = () => useStore(logger);
|
export const useLogData = () => useStore(logger);
|
||||||
|
|
||||||
export const addLog = (log: Log) =>
|
export const addLog = (log: Log) =>
|
||||||
logger.setState((state) => ({
|
logger.setState((state) => ({
|
||||||
logs: [log, ...state.logs],
|
logs: [log, ...state.logs].slice(0, maxLogEntries),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const clearLogs = () => logger.setState({ logs: [] });
|
export const clearLogs = () => logger.setState({ logs: [] });
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ $inner-padding: 1rem;
|
|||||||
td[data-warn='true'] {
|
td[data-warn='true'] {
|
||||||
background-color: $orange-1300;
|
background-color: $orange-1300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr[data-highlight='true'] {
|
||||||
|
background-color: $blue-1100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.listGroup {
|
.listGroup {
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ export default function InfoNif() {
|
|||||||
// interfaces outside localhost wont have access
|
// interfaces outside localhost wont have access
|
||||||
if (nif.name === 'localhost' && !isLocalhost) return null;
|
if (nif.name === 'localhost' && !isLocalhost) return null;
|
||||||
const address = linkToOtherHost(nif.address);
|
const address = linkToOtherHost(nif.address);
|
||||||
|
// we show the address as it would be typed in a browser, including the port
|
||||||
|
const label = new URL(address).host;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyTag key={nif.name} copyValue={address} onClick={() => handleClick(address)}>
|
<CopyTag key={`${nif.name}-${nif.address}`} copyValue={address} onClick={() => handleClick(address)}>
|
||||||
{`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
|
{`${nif.name} - ${label}`} <IoArrowUp className={style.goIcon} />
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ export default function LogExport() {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Event log
|
Event log
|
||||||
<Button onClick={extract}>
|
<Button onClick={extract}>
|
||||||
Extract <IoArrowUp className={style.iconRotate} />
|
Open in new window <IoArrowUp className={style.iconRotate} />
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
|
<Panel.Paragraph>
|
||||||
|
Activity in this session. The log is not saved with the project and is cleared when Ontime restarts.
|
||||||
|
</Panel.Paragraph>
|
||||||
<Log />
|
<Log />
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
import { MessageTag } from 'ontime-types';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { usePing } from '../../../../common/hooks/useSocket';
|
import { isOntimeCloud } from '../../../../externals';
|
||||||
import { sendSocket } from '../../../../common/utils/socket';
|
|
||||||
import { isDocker } from '../../../../externals';
|
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import ClientControlPanel from './client-control/ClientControlPanel';
|
import ClientControlPanel from './client-control/ClientControlPanel';
|
||||||
|
import InfoNif from './NetworkInterfaces';
|
||||||
import LogExport from './NetworkLogExport';
|
import LogExport from './NetworkLogExport';
|
||||||
|
import NetworkStatus from './NetworkStatus';
|
||||||
|
|
||||||
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||||
|
const statusRef = useScrollIntoView<HTMLDivElement>('status', location);
|
||||||
|
const interfacesRef = useScrollIntoView<HTMLDivElement>('interfaces', location);
|
||||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
||||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Network</Panel.Header>
|
<Panel.Header>Network</Panel.Header>
|
||||||
{isDocker && (
|
<div ref={statusRef}>
|
||||||
<Panel.Section>
|
<NetworkStatus />
|
||||||
<OntimeCloudStats />
|
</div>
|
||||||
</Panel.Section>
|
{!isOntimeCloud && (
|
||||||
|
<div ref={interfacesRef}>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>Network interfaces</Panel.SubHeader>
|
||||||
|
<Panel.Divider />
|
||||||
|
<Panel.Paragraph>
|
||||||
|
Addresses which can be used to reach Ontime from other devices in the network. <br />
|
||||||
|
Click an address to open it in a new window, or use the icon to copy it to the clipboard.
|
||||||
|
</Panel.Paragraph>
|
||||||
|
<InfoNif />
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={logRef}>
|
<div ref={logRef}>
|
||||||
<LogExport />
|
<LogExport />
|
||||||
@@ -31,29 +43,3 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OntimeCloudStats() {
|
|
||||||
const ping = usePing();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send immediate ping request, and keep sending on an interval
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
sendSocket(MessageTag.Ping, new Date());
|
|
||||||
|
|
||||||
const doPing = setInterval(() => {
|
|
||||||
sendSocket(MessageTag.Ping, new Date());
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(doPing);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.SubHeader>
|
|
||||||
Ontime cloud
|
|
||||||
<Panel.Description>Current ping: {ping}ms</Panel.Description>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { MaybeString, MessageTag } from 'ontime-types';
|
||||||
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
|
import useSessionStats from '../../../../common/hooks-query/useSessionStats';
|
||||||
|
import { usePing } from '../../../../common/hooks/useSocket';
|
||||||
|
import { useClientStore } from '../../../../common/stores/clientStore';
|
||||||
|
import { formatDuration } from '../../../../common/utils/time';
|
||||||
|
import { sendSocket } from '../../../../common/utils/socket';
|
||||||
|
import { isDocker } from '../../../../externals';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
/** ping values above this are flagged to the user (ms) */
|
||||||
|
const slowPingThreshold = 100;
|
||||||
|
|
||||||
|
/** how often we ask the server for a ping (ms) */
|
||||||
|
const pingInterval = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presents a date as the time elapsed since it happened
|
||||||
|
*/
|
||||||
|
function timeSince(date: MaybeString): string {
|
||||||
|
if (date === null) {
|
||||||
|
return 'Never';
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Date.now() - new Date(date).getTime();
|
||||||
|
if (elapsed < MILLIS_PER_MINUTE) {
|
||||||
|
return 'Just now';
|
||||||
|
}
|
||||||
|
return `${formatDuration(elapsed)} ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NetworkStatus() {
|
||||||
|
const { data, status, isError } = useSessionStats();
|
||||||
|
// the client list is kept up to date over websocket, unlike the polled session stats
|
||||||
|
const clients = useClientStore((store) => store.clients);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>Server status</Panel.SubHeader>
|
||||||
|
<Panel.Loader isLoading={status === 'pending'} />
|
||||||
|
{isError && <Panel.Error>Failed to load session data</Panel.Error>}
|
||||||
|
<Panel.Divider />
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Connected clients' description='Clients currently connected to this server' />
|
||||||
|
<Tag>{Object.keys(clients).length}</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Server running for' description='Time since the Ontime server was started' />
|
||||||
|
<Tag>{data ? formatDuration(Date.now() - new Date(data.startedAt).getTime()) : '...'}</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Last client connection' description='When a client last connected to the server' />
|
||||||
|
<Tag>{data ? timeSince(data.lastConnection) : '...'}</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Last integration request'
|
||||||
|
description='When the server last received a request from an integration'
|
||||||
|
/>
|
||||||
|
<Tag>{data ? timeSince(data.lastRequest) : '...'}</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Server time zone' description='Time zone used by the machine running Ontime' />
|
||||||
|
<Tag>{data ? data.timezone : '...'}</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
{isDocker && <CloudPing />}
|
||||||
|
</Panel.ListGroup>
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ontime cloud users are not in the same network as the server
|
||||||
|
* we show the round trip time to help them qualify the connection
|
||||||
|
*/
|
||||||
|
function CloudPing() {
|
||||||
|
const ping = usePing();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send immediate ping request, and keep sending on an interval
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
sendSocket(MessageTag.Ping, new Date());
|
||||||
|
|
||||||
|
const doPing = setInterval(() => {
|
||||||
|
sendSocket(MessageTag.Ping, new Date());
|
||||||
|
}, pingInterval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(doPing);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Ontime cloud latency'
|
||||||
|
description='Time for a message to travel to the server and back. Lower is better'
|
||||||
|
/>
|
||||||
|
<Tag variant={ping > slowPingThreshold ? 'warning' : 'default'}>{ping}ms</Tag>
|
||||||
|
</Panel.ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
+7
-10
@@ -1,11 +1,3 @@
|
|||||||
.fullWidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.halfWidth {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.halfWidthNoWrap {
|
.halfWidthNoWrap {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -16,6 +8,11 @@
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.self {
|
.muted {
|
||||||
background-color: $blue-1100;
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// align section notes with the table contents
|
||||||
|
.note {
|
||||||
|
padding: 0 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-11
@@ -5,14 +5,42 @@ import { useState } from 'react';
|
|||||||
import Button from '../../../../../common/components/buttons/Button';
|
import Button from '../../../../../common/components/buttons/Button';
|
||||||
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
|
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
|
||||||
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
|
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
|
||||||
|
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
||||||
import Tag from '../../../../../common/components/tag/Tag';
|
import Tag from '../../../../../common/components/tag/Tag';
|
||||||
|
import Tooltip from '../../../../../common/components/tooltip/Tooltip';
|
||||||
import { setClientRemote } from '../../../../../common/hooks/useSocket';
|
import { setClientRemote } from '../../../../../common/hooks/useSocket';
|
||||||
import { useClientStore } from '../../../../../common/stores/clientStore';
|
import { useClientStore } from '../../../../../common/stores/clientStore';
|
||||||
import { cx } from '../../../../../common/utils/styleUtils';
|
import { openLink } from '../../../../../common/utils/linkUtils';
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './ClientControlPanel.module.scss';
|
import style from './ClientControlPanel.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the path a client is showing
|
||||||
|
* The full URL is available for copying or opening in a new window
|
||||||
|
*/
|
||||||
|
function ClientPath({ origin, path }: { origin: string; path: string }) {
|
||||||
|
// clients report their path on connection, we may not have it yet
|
||||||
|
if (!path) {
|
||||||
|
return <span className={style.muted}>unknown</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// origin is empty for clients which have not reported their location
|
||||||
|
if (!origin) {
|
||||||
|
return <span className={style.copiable}>{path}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = `${origin}${path}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={fullUrl} render={<span />}>
|
||||||
|
<CopyTag copyValue={fullUrl} size='small' onClick={() => openLink(fullUrl)}>
|
||||||
|
{path}
|
||||||
|
</CopyTag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ClientList() {
|
export default function ClientList() {
|
||||||
const id = useClientStore((store) => store.id);
|
const id = useClientStore((store) => store.id);
|
||||||
const clients = useClientStore((store) => store.clients);
|
const clients = useClientStore((store) => store.clients);
|
||||||
@@ -32,8 +60,19 @@ export default function ClientList() {
|
|||||||
redirectHandler.open();
|
redirectHandler.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
|
/**
|
||||||
const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime');
|
* Clients are given by the server in connection order, which means rows move
|
||||||
|
* as clients come and go. We keep a stable order: self first, then by name
|
||||||
|
*/
|
||||||
|
const sortClients = (clientEntries: [string, Client][]) =>
|
||||||
|
clientEntries.sort(([keyA, clientA], [keyB, clientB]) => {
|
||||||
|
if (keyA === id) return -1;
|
||||||
|
if (keyB === id) return 1;
|
||||||
|
return clientA.name.localeCompare(clientB.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ontimeClients = sortClients(Object.entries(clients).filter(([_, { type }]) => type === 'ontime'));
|
||||||
|
const otherClients = sortClients(Object.entries(clients).filter(([_, { type }]) => type !== 'ontime'));
|
||||||
|
|
||||||
const targetClient: Client | undefined = clients[targetId];
|
const targetClient: Client | undefined = clients[targetId];
|
||||||
|
|
||||||
@@ -59,29 +98,37 @@ export default function ClientList() {
|
|||||||
)}
|
)}
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
|
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
|
||||||
|
<div className={style.note}>
|
||||||
|
<Panel.Description>
|
||||||
|
Ontime views connected to this server. Identify shows a marker in the client screen until you turn it off,
|
||||||
|
redirect sends the client to a different view.
|
||||||
|
</Panel.Description>
|
||||||
|
</div>
|
||||||
<Panel.Table>
|
<Panel.Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td style={{ width: '20%' }}>Client Name</td>
|
<th style={{ width: '20%' }}>Client Name</th>
|
||||||
<td>Path</td>
|
<th>Path</th>
|
||||||
<td />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{ontimeClients.length === 0 && <Panel.TableEmpty label='No Ontime clients connected' />}
|
||||||
{ontimeClients.map(([key, client]) => {
|
{ontimeClients.map(([key, client]) => {
|
||||||
const { identify, name, path } = client;
|
const { identify, name, path } = client;
|
||||||
const isCurrent = id === key;
|
const isCurrent = id === key;
|
||||||
return (
|
return (
|
||||||
<tr key={key} className={cx([isCurrent && style.self])}>
|
<tr key={key} data-highlight={isCurrent}>
|
||||||
<Panel.InlineElements relation='inner' as='td'>
|
<Panel.InlineElements relation='inner' as='td'>
|
||||||
{isCurrent && <Tag>SELF</Tag>}
|
{isCurrent && <Tag>SELF</Tag>}
|
||||||
{name}
|
{name}
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
<td className={style.copiable}>{path}</td>
|
<td>
|
||||||
|
<ClientPath origin={client.origin} path={path} />
|
||||||
|
</td>
|
||||||
<Panel.InlineElements relation='inner' as='td'>
|
<Panel.InlineElements relation='inner' as='td'>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
className={`${identify ? style.blink : ''}`}
|
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
variant={identify ? 'primary' : 'subtle'}
|
variant={identify ? 'primary' : 'subtle'}
|
||||||
data-testid={isCurrent ? '' : 'not-self-identify'}
|
data-testid={isCurrent ? '' : 'not-self-identify'}
|
||||||
@@ -117,14 +164,21 @@ export default function ClientList() {
|
|||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
|
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
|
||||||
|
<div className={style.note}>
|
||||||
|
<Panel.Description>
|
||||||
|
Connections which are not Ontime views, such as integrations and custom clients. These cannot be controlled
|
||||||
|
from Ontime.
|
||||||
|
</Panel.Description>
|
||||||
|
</div>
|
||||||
<Panel.Table>
|
<Panel.Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td className={style.halfWidthNoWrap}>Client Name</td>
|
<th className={style.halfWidthNoWrap}>Client Name</th>
|
||||||
<td className={style.halfWidthNoWrap}>Client type</td>
|
<th className={style.halfWidthNoWrap}>Client type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{otherClients.length === 0 && <Panel.TableEmpty label='No other clients connected' />}
|
||||||
{otherClients.map(([key, client]) => {
|
{otherClients.map(([key, client]) => {
|
||||||
const { name, type } = client;
|
const { name, type } = client;
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ const staticOptions = [
|
|||||||
id: 'network',
|
id: 'network',
|
||||||
label: 'Network',
|
label: 'Network',
|
||||||
secondary: [
|
secondary: [
|
||||||
|
{
|
||||||
|
id: 'network__status',
|
||||||
|
label: 'Server status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'network__interfaces',
|
||||||
|
label: 'Network interfaces',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'network__log',
|
id: 'network__log',
|
||||||
label: 'Event log',
|
label: 'Event log',
|
||||||
|
|||||||
@@ -51,3 +51,13 @@ $info-hover: $section-white;
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: $info-gray;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $info-gray;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,115 +1,90 @@
|
|||||||
import { LogOrigin } from 'ontime-types';
|
import { LogLevel, LogOrigin } from 'ontime-types';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
|
||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
|
import Input from '../../common/components/input/input/Input';
|
||||||
import { clearLogs, useLogData } from '../../common/stores/logger';
|
import { clearLogs, useLogData } from '../../common/stores/logger';
|
||||||
import * as Panel from '../app-settings/panel-utils/PanelUtils';
|
import * as Panel from '../app-settings/panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './Log.module.scss';
|
import style from './Log.module.scss';
|
||||||
|
|
||||||
|
const allOrigins = [
|
||||||
|
LogOrigin.User,
|
||||||
|
LogOrigin.Client,
|
||||||
|
LogOrigin.Server,
|
||||||
|
LogOrigin.Playback,
|
||||||
|
LogOrigin.Rx,
|
||||||
|
LogOrigin.Tx,
|
||||||
|
];
|
||||||
|
|
||||||
export default function Log() {
|
export default function Log() {
|
||||||
const { logs: logData } = useLogData();
|
const { logs: logData } = useLogData();
|
||||||
|
|
||||||
const [showClient, setShowClient] = useState(true);
|
// log entries are not guaranteed to have an origin from the enum
|
||||||
const [showServer, setShowServer] = useState(true);
|
const [origins, setOrigins] = useState<Set<string>>(() => new Set<string>(allOrigins));
|
||||||
const [showRx, setShowRx] = useState(true);
|
const [onlyProblems, setOnlyProblems] = useState(false);
|
||||||
const [showTx, setShowTx] = useState(true);
|
const [search, setSearch] = useState('');
|
||||||
const [showPlayback, setShowPlayback] = useState(true);
|
|
||||||
const [showUser, setShowUser] = useState(true);
|
|
||||||
|
|
||||||
const matchers: LogOrigin[] = [];
|
const toggleOrigin = useCallback((origin: LogOrigin) => {
|
||||||
if (showUser) {
|
setOrigins((previous) => {
|
||||||
matchers.push(LogOrigin.User);
|
const newOrigins = new Set(previous);
|
||||||
}
|
if (newOrigins.has(origin)) {
|
||||||
if (showClient) {
|
newOrigins.delete(origin);
|
||||||
matchers.push(LogOrigin.Client);
|
} else {
|
||||||
}
|
newOrigins.add(origin);
|
||||||
if (showServer) {
|
}
|
||||||
matchers.push(LogOrigin.Server);
|
return newOrigins;
|
||||||
}
|
});
|
||||||
if (showRx) {
|
|
||||||
matchers.push(LogOrigin.Rx);
|
|
||||||
}
|
|
||||||
if (showTx) {
|
|
||||||
matchers.push(LogOrigin.Tx);
|
|
||||||
}
|
|
||||||
if (showPlayback) {
|
|
||||||
matchers.push(LogOrigin.Playback);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
|
|
||||||
|
|
||||||
const disableOthers = useCallback((toEnable: LogOrigin) => {
|
|
||||||
setShowUser(toEnable === LogOrigin.User);
|
|
||||||
setShowClient(toEnable === LogOrigin.Client);
|
|
||||||
setShowServer(toEnable === LogOrigin.Server);
|
|
||||||
setShowRx(toEnable === LogOrigin.Rx);
|
|
||||||
setShowTx(toEnable === LogOrigin.Tx);
|
|
||||||
setShowPlayback(toEnable === LogOrigin.Playback);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** middle click on an origin shows only that origin */
|
||||||
|
const showOnlyOrigin = useCallback((origin: LogOrigin) => {
|
||||||
|
setOrigins(new Set([origin]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const searchTerm = search.toLowerCase();
|
||||||
|
const filteredData = logData.filter((entry) => {
|
||||||
|
if (!origins.has(entry.origin)) return false;
|
||||||
|
if (onlyProblems && entry.level === LogLevel.Info) return false;
|
||||||
|
if (searchTerm && !entry.text.toLowerCase().includes(searchTerm)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFiltered = filteredData.length !== logData.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.InlineElements className={style.buttonBar}>
|
<Panel.InlineElements className={style.buttonBar}>
|
||||||
|
{allOrigins.map((origin) => (
|
||||||
|
<Button
|
||||||
|
key={origin}
|
||||||
|
variant={origins.has(origin) ? 'primary' : 'subtle'}
|
||||||
|
size='small'
|
||||||
|
onClick={() => toggleOrigin(origin)}
|
||||||
|
onAuxClick={() => showOnlyOrigin(origin)}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{origin}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
<Button
|
<Button
|
||||||
variant={showUser ? 'primary' : 'subtle'}
|
variant={onlyProblems ? 'primary' : 'subtle'}
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => setShowUser((s) => !s)}
|
onClick={() => setOnlyProblems((previous) => !previous)}
|
||||||
onAuxClick={() => disableOthers(LogOrigin.User)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
>
|
||||||
{LogOrigin.User}
|
Issues only
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={showClient ? 'primary' : 'subtle'}
|
|
||||||
size='small'
|
|
||||||
onClick={() => setShowClient((s) => !s)}
|
|
||||||
onAuxClick={() => disableOthers(LogOrigin.Client)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{LogOrigin.Client}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={showServer ? 'primary' : 'subtle'}
|
|
||||||
size='small'
|
|
||||||
onClick={() => setShowServer((s) => !s)}
|
|
||||||
onAuxClick={() => disableOthers(LogOrigin.Server)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{LogOrigin.Server}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={showPlayback ? 'primary' : 'subtle'}
|
|
||||||
size='small'
|
|
||||||
onClick={() => setShowPlayback((s) => !s)}
|
|
||||||
onAuxClick={() => disableOthers(LogOrigin.Playback)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{LogOrigin.Playback}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={showRx ? 'primary' : 'subtle'}
|
|
||||||
size='small'
|
|
||||||
onClick={() => setShowRx((s) => !s)}
|
|
||||||
onAuxClick={() => disableOthers(LogOrigin.Rx)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{LogOrigin.Rx}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={showTx ? 'primary' : 'subtle'}
|
|
||||||
size='small'
|
|
||||||
onClick={() => setShowTx((s) => !s)}
|
|
||||||
onAuxClick={() => disableOthers(LogOrigin.Tx)}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{LogOrigin.Tx}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}>
|
<Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}>
|
||||||
<IoClose /> Clear
|
<IoClose /> Clear
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
|
<Panel.InlineElements className={style.buttonBar}>
|
||||||
|
<Input fluid placeholder='Filter messages' value={search} onChange={(event) => setSearch(event.target.value)} />
|
||||||
|
<span className={style.count}>
|
||||||
|
{isFiltered ? `${filteredData.length} of ${logData.length}` : logData.length} entries
|
||||||
|
</span>
|
||||||
|
</Panel.InlineElements>
|
||||||
<ul className={style.log}>
|
<ul className={style.log}>
|
||||||
{filteredData.map((logEntry) => (
|
{filteredData.map((logEntry) => (
|
||||||
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
|
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
|
||||||
@@ -118,6 +93,9 @@ export default function Log() {
|
|||||||
<span className={style.msg}>{logEntry.text}</span>
|
<span className={style.msg}>{logEntry.text}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
{filteredData.length === 0 && (
|
||||||
|
<li className={style.empty}>{logData.length === 0 ? 'No activity yet' : 'No entries match the filters'}</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Day, Duration, Instant, TimeOfDay } from 'ontime-types';
|
import { Day, Duration, Instant, TimeOfDay } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
/** Returns the current instant */
|
/** Returns the current instant */
|
||||||
export function now(): Instant {
|
export function now(): Instant {
|
||||||
@@ -59,10 +59,6 @@ export function addDuration(instant: Instant, duration: Duration | Duration[]):
|
|||||||
return (instant + totalDuration) as Instant;
|
return (instant + totalDuration) as Instant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function combineValues<T extends Duration | Day>(...duration: T[]): T {
|
|
||||||
return Array.isArray(duration) ? (duration.reduce<number>((total, current) => total + current, 0) as T) : duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates elapsed time on the clock from a starting time to the current time
|
* Calculates elapsed time on the clock from a starting time to the current time
|
||||||
* Handles overnight crossing (when current < start, assumes we've crossed midnight)
|
* Handles overnight crossing (when current < start, assumes we've crossed midnight)
|
||||||
@@ -84,15 +80,3 @@ export function daysSinceStart(startEpoch: Instant, currentEpoch: Instant): Day
|
|||||||
|
|
||||||
return (currentDaySerial - startDaySerial) as Day;
|
return (currentDaySerial - startDaySerial) as Day;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZERO_DURATION = 0 as Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* utility function to generate Instant times
|
|
||||||
*/
|
|
||||||
export function generateInstanceFromClock(hour: number, minutes: number, second: number, ms = 0) {
|
|
||||||
return toInstant(
|
|
||||||
(hour * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + second * MILLIS_PER_SECOND + ms) as TimeOfDay,
|
|
||||||
now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Duration, EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import * as timeCore from '../../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../../stores/runtimeState.js';
|
|
||||||
import {
|
import {
|
||||||
findDayOffset,
|
findDayOffset,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
@@ -20,48 +19,48 @@ const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeSt
|
|||||||
describe('getElapsed()', () => {
|
describe('getElapsed()', () => {
|
||||||
it('returns active elapsed time from startedAt without add-time adjustments', () => {
|
it('returns active elapsed time from startedAt without add-time adjustments', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 5, 0),
|
clock: 5 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: -10 * MILLIS_PER_MINUTE,
|
addedTime: -10 * MILLIS_PER_MINUTE,
|
||||||
current: 15 * MILLIS_PER_MINUTE,
|
current: 15 * MILLIS_PER_MINUTE,
|
||||||
duration: 20 * MILLIS_PER_MINUTE,
|
duration: 20 * MILLIS_PER_MINUTE,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0,
|
pausedDuration: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('subtracts accumulated pause time', () => {
|
it('subtracts accumulated pause time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 10, 0),
|
clock: 10 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 5 * MILLIS_PER_MINUTE,
|
pausedDuration: 5 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the current pause start while paused', () => {
|
it('uses the current pause start while paused', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 10, 0),
|
clock: 10 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: 7 * MILLIS_PER_MINUTE,
|
pausedAt: 7 * MILLIS_PER_MINUTE,
|
||||||
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
@@ -83,7 +82,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(null);
|
expect(calculatedFinish).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -96,13 +95,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: true,
|
hasFinished: true,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(20);
|
expect(calculatedFinish).toBe(20);
|
||||||
});
|
});
|
||||||
@@ -115,13 +114,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(11);
|
expect(calculatedFinish).toBe(11);
|
||||||
});
|
});
|
||||||
@@ -134,13 +133,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 20,
|
addedTime: 20,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(31);
|
expect(calculatedFinish).toBe(31);
|
||||||
@@ -154,13 +153,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: -10,
|
addedTime: -10,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -174,13 +173,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: -100,
|
addedTime: -100,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -194,13 +193,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -214,13 +213,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs,
|
duration: dayInMs,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(10);
|
expect(calculatedFinish).toBe(10);
|
||||||
@@ -235,13 +234,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: dayInMs,
|
duration: dayInMs,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(40);
|
expect(calculatedFinish).toBe(40);
|
||||||
@@ -254,7 +253,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
@@ -264,7 +263,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
actualStart: 79200000,
|
actualStart: 79200000,
|
||||||
plannedEnd: 600000,
|
plannedEnd: 600000,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
// expected finish is not a duration but a point in time
|
// expected finish is not a duration but a point in time
|
||||||
@@ -280,7 +279,7 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 30,
|
timeEnd: 30,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0),
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: 111, // <-- we take the duration value
|
duration: 111, // <-- we take the duration value
|
||||||
@@ -290,7 +289,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(111);
|
expect(current).toBe(111);
|
||||||
@@ -301,17 +300,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
clock: 1,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(9);
|
expect(current).toBe(9);
|
||||||
@@ -322,17 +321,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
clock: 1,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(19);
|
expect(current).toBe(19);
|
||||||
@@ -343,17 +342,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
clock: 10,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(dayInMs + 10);
|
expect(current).toBe(dayInMs + 10);
|
||||||
@@ -364,17 +363,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(15);
|
expect(current).toBe(15);
|
||||||
@@ -385,17 +384,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 20,
|
addedTime: 20,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(35);
|
expect(current).toBe(35);
|
||||||
@@ -408,7 +407,7 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
@@ -421,7 +420,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(70);
|
expect(current).toBe(70);
|
||||||
@@ -433,11 +432,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
plannedEnd: 100,
|
plannedEnd: 100,
|
||||||
@@ -446,7 +445,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(70);
|
expect(current).toBe(70);
|
||||||
@@ -458,11 +457,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 7,
|
addedTime: 7,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
plannedEnd: 100,
|
plannedEnd: 100,
|
||||||
@@ -471,7 +470,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(77);
|
expect(current).toBe(77);
|
||||||
@@ -484,11 +483,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 600000, // 00:10:00
|
timeEnd: 600000, // 00:10:00
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(22, 5, 0),
|
clock: 79500000, // 22:05:00
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: Infinity, // not relevant,
|
duration: Infinity, // not relevant,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 79200000,
|
actualStart: 79200000,
|
||||||
@@ -498,7 +497,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(dayInMs - 79500000 + 600000);
|
expect(current).toBe(dayInMs - 79500000 + 600000);
|
||||||
@@ -506,7 +505,7 @@ describe('getCurrent()', () => {
|
|||||||
|
|
||||||
it('handles events that were started late', () => {
|
it('handles events that were started late', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16 min after the scheduled end
|
clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
timeEnd: 81000000, // 22:30:00
|
timeEnd: 81000000, // 22:30:00
|
||||||
@@ -516,7 +515,7 @@ describe('getCurrent()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: Infinity, // not relevant,
|
duration: Infinity, // not relevant,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 82000000, // 22:46:40 <--- started now
|
actualStart: 82000000, // 22:46:40 <--- started now
|
||||||
@@ -526,7 +525,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(81000000 - 82000000); // <-- planned end - now
|
expect(current).toBe(81000000 - 82000000); // <-- planned end - now
|
||||||
@@ -542,17 +541,17 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0),
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration,
|
duration,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const expectedFinish = getExpectedFinish(state);
|
const expectedFinish = getExpectedFinish(state);
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
@@ -570,25 +569,25 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 3,
|
addedTime: 3,
|
||||||
duration,
|
duration,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const expectedFinish = getExpectedFinish(state);
|
const expectedFinish = getExpectedFinish(state);
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
|
|
||||||
const elapsed = duration - current;
|
const elapsed = duration - current;
|
||||||
expect(expectedFinish).toBe(13);
|
expect(expectedFinish).toBe(13);
|
||||||
expect(current).toBe(8);
|
|
||||||
expect(elapsed).toBe(2);
|
expect(elapsed).toBe(2);
|
||||||
|
expect(current).toBe(8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -872,7 +871,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -884,9 +883,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
clock: 150,
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(50);
|
expect(absolute).toBe(50);
|
||||||
@@ -900,7 +899,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150), // we started 50ms delayed
|
startedAt: 150, // we started 50ms delayed
|
||||||
addedTime: 10, // we compensated with 10ms
|
addedTime: 10, // we compensated with 10ms
|
||||||
current: 10, // we are 10ms into the timer
|
current: 10, // we are 10ms into the timer
|
||||||
},
|
},
|
||||||
@@ -913,7 +912,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(60);
|
expect(absolute).toBe(60);
|
||||||
@@ -928,7 +927,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // we started ontime
|
startedAt: 100, // we started ontime
|
||||||
current: -10, // we are 10 seconds over
|
current: -10, // we are 10 seconds over
|
||||||
addedTime: 0, // we have not compensated with added time
|
addedTime: 0, // we have not compensated with added time
|
||||||
},
|
},
|
||||||
@@ -941,7 +940,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(10);
|
expect(absolute).toBe(10);
|
||||||
@@ -955,9 +954,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
timeEnd: 150,
|
timeEnd: 150,
|
||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
clock: 150,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // started on time
|
startedAt: 100, // started on time
|
||||||
current: 25, // are 25ms into it
|
current: 25, // are 25ms into it
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
},
|
},
|
||||||
@@ -970,7 +969,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(25);
|
expect(absolute).toBe(25);
|
||||||
@@ -978,7 +977,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('offset doesnt exist if we havent started', () => {
|
it('offset doesnt exist if we havent started', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 48, 0), // 78480789
|
clock: 78480789,
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
timeStart: 77400000,
|
timeStart: 77400000,
|
||||||
@@ -1012,7 +1011,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1020,7 +1019,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 13, 20),
|
clock: 80000000, // 22:13:20
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
@@ -1064,11 +1063,11 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 81600000,
|
expectedFinish: 81600000,
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1076,7 +1075,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('with time-to-end, offset is the overtime', () => {
|
it('with time-to-end, offset is the overtime', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40),
|
clock: 82000000, // 22:46:40
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
@@ -1120,11 +1119,11 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 81600000,
|
expectedFinish: 81600000,
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time
|
expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time
|
||||||
@@ -1132,7 +1131,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('handles time-to-end started after the end time', () => {
|
it('handles time-to-end started after the end time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16m 40s after the scheduled end
|
clock: 82000000, // 22:46:40 <--- starting 16m 40s after the scheduled end
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
@@ -1165,14 +1164,14 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 82000000 + 3600000, // <--- now + duration
|
expectedFinish: 82000000 + 3600000, // <--- now + duration
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 46, 40),
|
startedAt: 82000000, // <--- started now
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const updateCurrent = getCurrent(state);
|
const updateCurrent = getCurrent(state);
|
||||||
state.timer.current = updateCurrent as Duration;
|
state.timer.current = updateCurrent;
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(millisToString(absolute)).toBe('00:16:40');
|
expect(millisToString(absolute)).toBe('00:16:40');
|
||||||
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
|
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
|
||||||
@@ -1188,7 +1187,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1201,7 +1200,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1215,7 +1214,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1228,7 +1227,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(50);
|
expect(absolute).toBe(50);
|
||||||
@@ -1242,7 +1241,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100),
|
startedAt: 100,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1255,7 +1254,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(-50);
|
expect(absolute).toBe(-50);
|
||||||
@@ -1277,7 +1276,7 @@ describe('getTimerPhase()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.None);
|
expect(phase).toBe(TimerPhase.None);
|
||||||
@@ -1295,7 +1294,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Overtime);
|
expect(phase).toBe(TimerPhase.Overtime);
|
||||||
@@ -1313,7 +1312,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Danger);
|
expect(phase).toBe(TimerPhase.Danger);
|
||||||
@@ -1331,7 +1330,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Warning);
|
expect(phase).toBe(TimerPhase.Warning);
|
||||||
@@ -1349,7 +1348,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Default);
|
expect(phase).toBe(TimerPhase.Default);
|
||||||
@@ -1357,7 +1356,7 @@ describe('getTimerPhase()', () => {
|
|||||||
|
|
||||||
it('#1042 identifies waiting to roll', () => {
|
it('#1042 identifies waiting to roll', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
|
clock: 55691050,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -1383,13 +1382,13 @@ describe('getTimerPhase()', () => {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Pending);
|
expect(phase).toBe(TimerPhase.Pending);
|
||||||
@@ -1397,7 +1396,7 @@ describe('getTimerPhase()', () => {
|
|||||||
|
|
||||||
it('#1042 identifies waiting to roll', () => {
|
it('#1042 identifies waiting to roll', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
|
clock: 55691050,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -1423,13 +1422,13 @@ describe('getTimerPhase()', () => {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Pending);
|
expect(phase).toBe(TimerPhase.Pending);
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
|||||||
import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js';
|
import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js';
|
||||||
import { normaliseEndTime } from './timerUtils.js';
|
import { normaliseEndTime } from './timerUtils.js';
|
||||||
|
|
||||||
// import * as timeCore from '../lib/time-core/timeCore.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds current event in a rolling rundown
|
* Finds current event in a rolling rundown
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -760,17 +760,16 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
|
|
||||||
// save the restore state
|
// save the restore state
|
||||||
if (hasImmediateChanges) {
|
if (hasImmediateChanges) {
|
||||||
const internalState = runtimeState.getInternalState();
|
|
||||||
restoreService
|
restoreService
|
||||||
.save({
|
.save({
|
||||||
playback: state.timer.playback,
|
playback: state.timer.playback,
|
||||||
selectedEventId: state.eventNow?.id ?? null,
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
startedAt: state.timer.startedAt,
|
startedAt: state.timer.startedAt,
|
||||||
addedTime: state.timer.addedTime,
|
addedTime: state.timer.addedTime,
|
||||||
pausedAt: internalState._timer.pausedAt,
|
pausedAt: state._timer.pausedAt,
|
||||||
pausedDuration: internalState._timer.pausedDuration,
|
pausedDuration: state._timer.pausedDuration,
|
||||||
firstStart: state.rundown.actualStart,
|
firstStart: state.rundown.actualStart,
|
||||||
startEpoch: internalState._startEpoch,
|
startEpoch: state._startEpoch,
|
||||||
currentDay: state.rundown.currentDay,
|
currentDay: state.rundown.currentDay,
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch((_e) => {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Day, Duration, Maybe, TimeOfDay, TimerPhase } from 'ontime-types';
|
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
|
|
||||||
import * as timeCore from '../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
import { ZERO_DURATION } from '../lib/time-core/timeCore.js';
|
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../stores/runtimeState.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle events that span over midnight
|
* handle events that span over midnight
|
||||||
@@ -22,46 +20,48 @@ export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates expected finish time of a running timer
|
* Calculates expected finish time of a running timer
|
||||||
* @param {InternalRuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
* @returns {number | null} new current time or null if nothing is running
|
* @returns {number | null} new current time or null if nothing is running
|
||||||
*/
|
*/
|
||||||
export function getExpectedFinish(state: InternalRuntimeState): Maybe<TimeOfDay> {
|
export function getExpectedFinish(state: RuntimeState): MaybeNumber {
|
||||||
const { startedAt, duration, addedTime } = state.timer;
|
const { startedAt, duration, addedTime } = state.timer;
|
||||||
|
|
||||||
if (state.eventNow === null) {
|
if (state.eventNow === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countToEnd, timeEnd } = state.eventNow;
|
const { countToEnd, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pausedTime = pausedAt !== null ? timeCore.toTimeOfDay(_now) - pausedAt : 0;
|
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
|
||||||
|
|
||||||
if (countToEnd) {
|
if (countToEnd) {
|
||||||
return (timeEnd + addedTime + pausedTime) as TimeOfDay;
|
return timeEnd + addedTime + pausedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle events that finish the day after
|
// handle events that finish the day after
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if there is a timer
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer
|
||||||
const expectedFinish = timeCore.toTimeOfDay(startedAt) + duration! + addedTime + pausedTime;
|
const expectedFinish = startedAt + duration! + addedTime + pausedTime;
|
||||||
|
|
||||||
if (expectedFinish > dayInMs) {
|
if (expectedFinish > dayInMs) {
|
||||||
return (expectedFinish - dayInMs) as TimeOfDay;
|
return expectedFinish - dayInMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// an event cannot finish before it started (user added too much negative time)
|
// an event cannot finish before it started (user added too much negative time)
|
||||||
return Math.max(expectedFinish, timeCore.toTimeOfDay(startedAt)) as TimeOfDay;
|
return Math.max(expectedFinish, startedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates running countdown
|
* Calculates running countdown
|
||||||
* @param {InternalRuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
* @returns {number} current time for timer
|
* @returns {number} current time for timer
|
||||||
*/
|
*/
|
||||||
export function getCurrent(state: InternalRuntimeState): Duration {
|
|
||||||
|
export function getCurrent(state: RuntimeState): number {
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
DEV: {
|
DEV: {
|
||||||
if (state.eventNow === null || state.timer.duration === null) {
|
if (state.eventNow === null || state.timer.duration === null) {
|
||||||
@@ -71,12 +71,12 @@ export function getCurrent(state: InternalRuntimeState): Duration {
|
|||||||
const { startedAt, duration, addedTime } = state.timer;
|
const { startedAt, duration, addedTime } = state.timer;
|
||||||
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (countToEnd) {
|
if (countToEnd) {
|
||||||
const isEventOverMidnight = timeStart > timeEnd;
|
const isEventOverMidnight = timeStart > timeEnd;
|
||||||
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
||||||
return (correctDay - timeCore.toTimeOfDay(_now) + timeEnd + addedTime) as Duration;
|
return correctDay - clock + timeEnd + addedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
@@ -84,19 +84,19 @@ export function getCurrent(state: InternalRuntimeState): Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pausedAt != null) {
|
if (pausedAt != null) {
|
||||||
return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - pausedAt) as Duration;
|
return startedAt + duration + addedTime - pausedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPassedMidnight = timeCore.toTimeOfDay(startedAt) > timeCore.toTimeOfDay(_now);
|
const hasPassedMidnight = startedAt > clock;
|
||||||
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
||||||
return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - timeCore.toTimeOfDay(_now) - correctDay) as Duration;
|
return startedAt + duration + addedTime - clock - correctDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates active time elapsed since the timer started.
|
* Calculates active time elapsed since the timer started.
|
||||||
*/
|
*/
|
||||||
export function getElapsed(state: InternalRuntimeState): Maybe<Duration> {
|
export function getElapsed(state: RuntimeState): MaybeNumber {
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
const { startedAt } = state.timer;
|
const { startedAt } = state.timer;
|
||||||
const { pausedAt, pausedDuration } = state._timer;
|
const { pausedAt, pausedDuration } = state._timer;
|
||||||
|
|
||||||
@@ -104,11 +104,11 @@ export function getElapsed(state: InternalRuntimeState): Maybe<Duration> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceClock = pausedAt ?? timeCore.toTimeOfDay(_now);
|
const referenceClock = pausedAt ?? clock;
|
||||||
const elapsedSinceStart = getTimeSinceStart(referenceClock, timeCore.toTimeOfDay(startedAt));
|
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
|
||||||
const activeElapsed = elapsedSinceStart - pausedDuration;
|
const activeElapsed = elapsedSinceStart - pausedDuration;
|
||||||
|
|
||||||
return Math.max(0, activeElapsed) as Duration;
|
return Math.max(0, activeElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
||||||
@@ -127,7 +127,7 @@ function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
||||||
// we cant have skipped if we haven't started
|
// we cant have skipped if we havent started
|
||||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -151,12 +151,12 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
|||||||
* Positive offset is over time / behind schedule
|
* Positive offset is over time / behind schedule
|
||||||
* Negative offset is under time / ahead of schedule
|
* Negative offset is under time / ahead of schedule
|
||||||
*/
|
*/
|
||||||
export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Duration; relative: Duration } {
|
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
|
||||||
const { eventNow, _now, _startDayOffset } = state;
|
const { eventNow, clock, _startDayOffset } = state;
|
||||||
const { addedTime, current, startedAt } = state.timer;
|
const { addedTime, current, startedAt } = state.timer;
|
||||||
// nothing to calculate if there are no loaded events or if we havent started
|
// nothing to calculate if there are no loaded events or if we havent started
|
||||||
if (eventNow === null || startedAt === null || _startDayOffset === null) {
|
if (eventNow === null || startedAt === null || _startDayOffset === null) {
|
||||||
return { absolute: ZERO_DURATION, relative: ZERO_DURATION };
|
return { absolute: 0, relative: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countToEnd, timeStart, dayOffset } = eventNow;
|
const { countToEnd, timeStart, dayOffset } = eventNow;
|
||||||
@@ -171,23 +171,20 @@ export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Durat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// difference between planned event start and actual event start (will be positive if we started behind)
|
// difference between planned event start and actual event start (will be positive if we started behind)
|
||||||
const eventStartOffset =
|
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
|
||||||
timeCore.toTimeOfDay(startedAt) + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
|
|
||||||
|
|
||||||
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
|
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
|
||||||
const overtime = Math.abs(Math.min(current, 0));
|
const overtime = Math.abs(Math.min(current, 0));
|
||||||
|
|
||||||
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
|
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
|
||||||
const pausedTime = state._timer.pausedAt === null ? 0 : timeCore.toTimeOfDay(_now) - state._timer.pausedAt;
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||||
|
|
||||||
// absolute offset is difference between schedule and playback time
|
// absolute offset is difference between schedule and playback time
|
||||||
// in case of count to end, the absolute offset is overtime and added time
|
// in case of count to end, the absolute offset is overtime and added time
|
||||||
const absolute = (
|
const absolute = countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime;
|
||||||
countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime
|
|
||||||
) as Duration;
|
|
||||||
|
|
||||||
// the relative offset is the same as the absolute but adjusted relative to the actual start time
|
// the relative offset is the same as the absolute but adjusted relative to the actual start time
|
||||||
const relative = (absolute + plannedStart - actualStart - _startDayOffset * dayInMs) as Duration;
|
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs;
|
||||||
|
|
||||||
return { absolute, relative };
|
return { absolute, relative };
|
||||||
}
|
}
|
||||||
@@ -196,7 +193,7 @@ export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Durat
|
|||||||
* Checks running timer to see which phase it currently is in
|
* Checks running timer to see which phase it currently is in
|
||||||
* @param state
|
* @param state
|
||||||
*/
|
*/
|
||||||
export function getTimerPhase(state: InternalRuntimeState): TimerPhase {
|
export function getTimerPhase(state: RuntimeState): TimerPhase {
|
||||||
if (!isPlaybackActive(state.timer.playback)) {
|
if (!isPlaybackActive(state.timer.playback)) {
|
||||||
return TimerPhase.None;
|
return TimerPhase.None;
|
||||||
}
|
}
|
||||||
@@ -226,7 +223,7 @@ export function getTimerPhase(state: InternalRuntimeState): TimerPhase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the day offset relative to an event start
|
* Finds the day offset relative to an event start
|
||||||
* used by the RuntimeState on first start to get correct offsets
|
* used byt the runtimeState on first start to get correct offsets
|
||||||
*/
|
*/
|
||||||
export function findDayOffset(plannedStart: number, clock: number): Day {
|
export function findDayOffset(plannedStart: number, clock: number): Day {
|
||||||
const distance = clock - plannedStart;
|
const distance = clock - plannedStart;
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
import { Day, type Duration, type Instant, OffsetMode, Playback, TimeOfDay, TimerPhase } from 'ontime-types';
|
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { deepmerge } from 'ontime-utils';
|
import { deepmerge } from 'ontime-utils';
|
||||||
|
|
||||||
import { ZERO_DURATION } from '../../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../runtimeState.js';
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../runtimeState.js';
|
|
||||||
|
|
||||||
export const runtimePlaceholder = {
|
const baseState: RuntimeState = {
|
||||||
|
clock: 0 as TimeOfDay,
|
||||||
|
eventNow: null,
|
||||||
|
eventNext: null,
|
||||||
|
eventFlag: null,
|
||||||
|
groupNow: null,
|
||||||
|
rundown: {
|
||||||
|
selectedEventIndex: null,
|
||||||
|
numEvents: 0,
|
||||||
|
plannedStart: 0,
|
||||||
|
plannedEnd: 0,
|
||||||
|
actualStart: null,
|
||||||
|
actualGroupStart: null,
|
||||||
|
currentDay: 0,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
absolute: 0,
|
||||||
|
relative: 0,
|
||||||
|
mode: OffsetMode.Absolute,
|
||||||
|
expectedRundownEnd: null,
|
||||||
|
expectedGroupEnd: null,
|
||||||
|
expectedFlagStart: null,
|
||||||
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: ZERO_DURATION,
|
addedTime: 0,
|
||||||
current: null,
|
current: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
elapsed: null,
|
elapsed: null,
|
||||||
@@ -16,43 +37,10 @@ export const runtimePlaceholder = {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
offset: {
|
|
||||||
absolute: ZERO_DURATION,
|
|
||||||
relative: ZERO_DURATION,
|
|
||||||
mode: OffsetMode.Absolute,
|
|
||||||
expectedGroupEnd: null,
|
|
||||||
expectedRundownEnd: null,
|
|
||||||
expectedFlagStart: null,
|
|
||||||
},
|
|
||||||
rundown: {
|
|
||||||
selectedEventIndex: null,
|
|
||||||
numEvents: 0,
|
|
||||||
plannedStart: 0 as TimeOfDay,
|
|
||||||
plannedEnd: 0 as TimeOfDay,
|
|
||||||
actualStart: null,
|
|
||||||
actualGroupStart: null,
|
|
||||||
|
|
||||||
currentDay: 0 as Day,
|
|
||||||
},
|
|
||||||
} as Readonly<InternalRuntimeState>;
|
|
||||||
|
|
||||||
const baseState: InternalRuntimeState = {
|
|
||||||
_now: 0 as Instant,
|
|
||||||
eventNow: null,
|
|
||||||
eventNext: null,
|
|
||||||
eventFlag: null,
|
|
||||||
groupNow: null,
|
|
||||||
rundown: {
|
|
||||||
...runtimePlaceholder.rundown,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
...runtimePlaceholder.offset,
|
|
||||||
},
|
|
||||||
timer: { ...runtimePlaceholder.timer },
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0 as Duration,
|
pausedDuration: 0,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
|||||||
import { initRundown } from '../../api-data/rundown/rundown.service.js';
|
import { initRundown } from '../../api-data/rundown/rundown.service.js';
|
||||||
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
||||||
import {
|
import {
|
||||||
type InternalRuntimeState,
|
type RuntimeState,
|
||||||
addTime,
|
addTime,
|
||||||
clearState,
|
clearState,
|
||||||
getInternalState,
|
|
||||||
getState,
|
getState,
|
||||||
load,
|
load,
|
||||||
loadGroupFlagAndEnd,
|
loadGroupFlagAndEnd,
|
||||||
@@ -33,7 +32,7 @@ const mockEvent = {
|
|||||||
} as PlayableEvent;
|
} as PlayableEvent;
|
||||||
|
|
||||||
const mockState = {
|
const mockState = {
|
||||||
_now: 666,
|
clock: 666,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -54,7 +53,7 @@ const mockState = {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
||||||
return {
|
return {
|
||||||
@@ -109,34 +108,34 @@ describe('mutation on runtimeState', () => {
|
|||||||
|
|
||||||
const { metadata, rundown } = rundownCache.get();
|
const { metadata, rundown } = rundownCache.get();
|
||||||
load(mockEvent, rundown, metadata);
|
load(mockEvent, rundown, metadata);
|
||||||
let newState = getInternalState();
|
let newState = getState();
|
||||||
expect(newState.eventNow?.id).toBe(mockEvent.id);
|
expect(newState.eventNow?.id).toBe(mockEvent.id);
|
||||||
expect(newState.eventNext?.id).toBe('event2');
|
expect(newState.eventNext?.id).toBe('event2');
|
||||||
expect(newState.timer.playback).toBe(Playback.Armed);
|
expect(newState.timer.playback).toBe(Playback.Armed);
|
||||||
expect(getState().clock).not.toBe(666);
|
expect(newState.clock).not.toBe(666);
|
||||||
expect(newState.groupNow).toBeNull();
|
expect(newState.groupNow).toBeNull();
|
||||||
|
|
||||||
// 2. Start event
|
// 2. Start event
|
||||||
vi.setSystemTime('jan 1 00:02');
|
vi.setSystemTime('jan 1 00:02');
|
||||||
let success = start();
|
let success = start();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
});
|
});
|
||||||
expect(newState.rundown.actualStart).toBe(getState().clock);
|
expect(newState.rundown.actualStart).toBe(newState.clock);
|
||||||
|
|
||||||
// 3. Pause event
|
// 3. Pause event
|
||||||
vi.setSystemTime('jan 1 00:03');
|
vi.setSystemTime('jan 1 00:03');
|
||||||
success = pause();
|
success = pause();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(getState().clock).not.toBe(666);
|
expect(newState.clock).not.toBe(666);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Pause,
|
playback: Playback.Pause,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
});
|
});
|
||||||
expect(newState._timer.pausedAt).toEqual(getState().clock);
|
expect(newState._timer.pausedAt).toEqual(newState.clock);
|
||||||
|
|
||||||
success = pause();
|
success = pause();
|
||||||
expect(success).toBe(false);
|
expect(success).toBe(false);
|
||||||
@@ -144,7 +143,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
// 4. Restart event
|
// 4. Restart event
|
||||||
vi.setSystemTime('jan 1 00:04');
|
vi.setSystemTime('jan 1 00:04');
|
||||||
success = start();
|
success = start();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
@@ -164,7 +163,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
// 5. Stop event
|
// 5. Stop event
|
||||||
vi.setSystemTime('jan 1 00:05');
|
vi.setSystemTime('jan 1 00:05');
|
||||||
success = stop();
|
success = stop();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.eventNow).toBe(null);
|
expect(newState.eventNow).toBe(null);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
@@ -826,7 +825,7 @@ describe('roll mode', () => {
|
|||||||
test('pending event', () => {
|
test('pending event', () => {
|
||||||
const { rundown, metadata } = rundownCache.get();
|
const { rundown, metadata } = rundownCache.get();
|
||||||
const { eventId, didStart } = roll(rundown, metadata);
|
const { eventId, didStart } = roll(rundown, metadata);
|
||||||
const state = getInternalState();
|
const state = getState();
|
||||||
|
|
||||||
expect(eventId).toBe('1');
|
expect(eventId).toBe('1');
|
||||||
expect(didStart).toBe(false);
|
expect(didStart).toBe(false);
|
||||||
@@ -945,7 +944,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[11],
|
eventNow: rundown.entries[11],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -973,7 +972,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[22],
|
eventNow: rundown.entries[22],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1001,7 +1000,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1026,7 +1025,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1088,7 +1087,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1170,7 +1169,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
TimerPhase,
|
TimerPhase,
|
||||||
TimerState,
|
TimerState,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
|
runtimeStorePlaceholder,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
calculateDuration,
|
calculateDuration,
|
||||||
@@ -43,18 +44,14 @@ import {
|
|||||||
hasCrossedMidnight,
|
hasCrossedMidnight,
|
||||||
} from '../services/timerUtils.js';
|
} from '../services/timerUtils.js';
|
||||||
import { timerConfig } from '../setup/config.js';
|
import { timerConfig } from '../setup/config.js';
|
||||||
import { runtimePlaceholder } from './__mocks__/runtimeState.mocks.js';
|
|
||||||
|
|
||||||
type ExpectedMetadata = {
|
type ExpectedMetadata = {
|
||||||
event: OntimeEvent;
|
event: OntimeEvent;
|
||||||
accumulatedGap: Duration;
|
accumulatedGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type RuntimeState = {
|
export type RuntimeState = {
|
||||||
/**
|
|
||||||
* @deprecated we want to phase out the 24h clock
|
|
||||||
*/
|
|
||||||
clock: TimeOfDay;
|
clock: TimeOfDay;
|
||||||
groupNow: OntimeGroup | null;
|
groupNow: OntimeGroup | null;
|
||||||
eventNow: PlayableEvent | null;
|
eventNow: PlayableEvent | null;
|
||||||
@@ -64,72 +61,36 @@ export type RuntimeState = {
|
|||||||
timer: TimerState;
|
timer: TimerState;
|
||||||
rundown: RundownState;
|
rundown: RundownState;
|
||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
};
|
|
||||||
|
|
||||||
export type InternalRuntimeState = Omit<RuntimeState, 'clock' | 'timer' | 'offset' | 'rundown'> & {
|
|
||||||
timer: {
|
|
||||||
addedTime: Duration;
|
|
||||||
current: Maybe<Duration>;
|
|
||||||
duration: Maybe<Duration>;
|
|
||||||
elapsed: Maybe<Duration>;
|
|
||||||
expectedFinish: Maybe<TimeOfDay>;
|
|
||||||
phase: TimerPhase;
|
|
||||||
playback: Playback;
|
|
||||||
secondaryTimer: Maybe<Duration>;
|
|
||||||
startedAt: Maybe<Instant>;
|
|
||||||
};
|
|
||||||
offset: {
|
|
||||||
absolute: Duration;
|
|
||||||
relative: Duration;
|
|
||||||
mode: OffsetMode;
|
|
||||||
expectedGroupEnd: Maybe<TimeOfDay>;
|
|
||||||
expectedRundownEnd: Maybe<TimeOfDay>;
|
|
||||||
expectedFlagStart: Maybe<TimeOfDay>;
|
|
||||||
};
|
|
||||||
rundown: {
|
|
||||||
selectedEventIndex: Maybe<number>;
|
|
||||||
numEvents: number;
|
|
||||||
plannedStart: Maybe<TimeOfDay>;
|
|
||||||
plannedEnd: Maybe<TimeOfDay>;
|
|
||||||
actualStart: Maybe<TimeOfDay>;
|
|
||||||
currentDay: Maybe<Day>;
|
|
||||||
actualGroupStart: Maybe<TimeOfDay>;
|
|
||||||
};
|
|
||||||
_now: Instant;
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: boolean; // whether we should declare an event as finished, will contain the finish time
|
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
||||||
pausedAt: Maybe<TimeOfDay>;
|
pausedAt: Maybe<TimeOfDay>;
|
||||||
pausedDuration: Duration;
|
pausedDuration: number;
|
||||||
secondaryTarget: Maybe<TimeOfDay>;
|
secondaryTarget: Maybe<TimeOfDay>;
|
||||||
hasFinished: boolean;
|
hasFinished: boolean;
|
||||||
};
|
};
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: number; // this value comes from rundown service
|
totalDelay: number; // this value comes from rundown service
|
||||||
};
|
};
|
||||||
_startDayOffset: Maybe<Day>;
|
|
||||||
_startEpoch: Maybe<Instant>;
|
|
||||||
_group: ExpectedMetadata;
|
_group: ExpectedMetadata;
|
||||||
_flag: ExpectedMetadata;
|
_flag: ExpectedMetadata;
|
||||||
_end: ExpectedMetadata;
|
_end: ExpectedMetadata;
|
||||||
|
_startEpoch: Maybe<Instant>;
|
||||||
|
_startDayOffset: Maybe<Day>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: InternalRuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
_now: timeCore.now(),
|
clock: timeCore.timeOfDayNow(),
|
||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
eventFlag: null,
|
eventFlag: null,
|
||||||
offset: { ...runtimePlaceholder.offset },
|
offset: { ...runtimeStorePlaceholder.offset },
|
||||||
timer: {
|
timer: { ...runtimeStorePlaceholder.timer },
|
||||||
...runtimePlaceholder.timer,
|
rundown: { ...runtimeStorePlaceholder.rundown },
|
||||||
},
|
|
||||||
rundown: {
|
|
||||||
...runtimePlaceholder.rundown,
|
|
||||||
},
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0 as Duration,
|
pausedDuration: 0,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
@@ -143,32 +104,19 @@ const runtimeState: InternalRuntimeState = {
|
|||||||
_startDayOffset: null,
|
_startDayOffset: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getInternalState(): Readonly<InternalRuntimeState> {
|
|
||||||
return runtimeState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
// create a shallow copy of the state
|
// create a shallow copy of the state
|
||||||
return {
|
return {
|
||||||
...runtimeState,
|
...runtimeState,
|
||||||
clock: timeCore.toTimeOfDay(runtimeState._now),
|
|
||||||
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
|
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
|
||||||
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
|
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
|
||||||
eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null,
|
eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null,
|
||||||
groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null,
|
groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null,
|
||||||
offset: { ...runtimeState.offset },
|
offset: { ...runtimeState.offset },
|
||||||
rundown: { ...runtimeState.rundown },
|
rundown: { ...runtimeState.rundown },
|
||||||
timer: {
|
timer: { ...runtimeState.timer },
|
||||||
addedTime: runtimeState.timer.addedTime,
|
_timer: { ...runtimeState._timer },
|
||||||
current: runtimeState.timer.current,
|
_rundown: { ...runtimeState._rundown },
|
||||||
duration: runtimeState.timer.duration,
|
|
||||||
elapsed: runtimeState.timer.elapsed,
|
|
||||||
expectedFinish: runtimeState.timer.expectedFinish,
|
|
||||||
phase: runtimeState.timer.phase,
|
|
||||||
playback: runtimeState.timer.playback,
|
|
||||||
secondaryTimer: runtimeState.timer.secondaryTimer,
|
|
||||||
startedAt: runtimeState.timer.startedAt ? timeCore.toTimeOfDay(runtimeState.timer.startedAt) : null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,18 +127,22 @@ export function clearEventData() {
|
|||||||
runtimeState.eventNow = null;
|
runtimeState.eventNow = null;
|
||||||
runtimeState.eventNext = null;
|
runtimeState.eventNext = null;
|
||||||
|
|
||||||
runtimeState.offset = { ...runtimePlaceholder.offset };
|
runtimeState.offset.absolute = 0;
|
||||||
|
runtimeState.offset.relative = 0;
|
||||||
|
runtimeState.offset.expectedFlagStart = null;
|
||||||
|
runtimeState.offset.expectedGroupEnd = null;
|
||||||
|
runtimeState.offset.expectedRundownEnd = null;
|
||||||
|
|
||||||
runtimeState.rundown.selectedEventIndex = null;
|
runtimeState.rundown.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState._now = timeCore.now();
|
runtimeState.clock = timeCore.timeOfDayNow();
|
||||||
runtimeState.timer = { ...runtimePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
runtimeState._timer.forceFinish = false;
|
runtimeState._timer.forceFinish = null;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
runtimeState._timer.pausedDuration = 0 as Duration;
|
runtimeState._timer.pausedDuration = 0;
|
||||||
runtimeState._timer.secondaryTarget = null;
|
runtimeState._timer.secondaryTarget = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
}
|
}
|
||||||
@@ -208,18 +160,22 @@ export function clearState() {
|
|||||||
runtimeState.rundown.actualStart = null;
|
runtimeState.rundown.actualStart = null;
|
||||||
runtimeState.rundown.selectedEventIndex = null;
|
runtimeState.rundown.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.offset = { ...runtimePlaceholder.offset };
|
runtimeState.offset.absolute = 0;
|
||||||
|
runtimeState.offset.relative = 0;
|
||||||
|
runtimeState.offset.expectedRundownEnd = null;
|
||||||
|
runtimeState.offset.expectedGroupEnd = null;
|
||||||
|
runtimeState.offset.expectedFlagStart = null;
|
||||||
|
|
||||||
runtimeState._end = null;
|
runtimeState._end = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState._now = timeCore.now();
|
runtimeState.clock = timeCore.timeOfDayNow();
|
||||||
runtimeState.timer = { ...runtimePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
runtimeState._timer.forceFinish = false;
|
runtimeState._timer.forceFinish = null;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
runtimeState._timer.pausedDuration = 0 as Duration;
|
runtimeState._timer.pausedDuration = 0;
|
||||||
runtimeState._timer.secondaryTarget = null;
|
runtimeState._timer.secondaryTarget = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
|
|
||||||
@@ -261,9 +217,9 @@ export function updateRundownData(rundownData: {
|
|||||||
runtimeState._rundown.totalDelay = rundownData.totalDelay;
|
runtimeState._rundown.totalDelay = rundownData.totalDelay;
|
||||||
|
|
||||||
runtimeState.rundown.numEvents = rundownData.numEvents;
|
runtimeState.rundown.numEvents = rundownData.numEvents;
|
||||||
runtimeState.rundown.plannedStart = rundownData.firstStart as TimeOfDay;
|
runtimeState.rundown.plannedStart = rundownData.firstStart;
|
||||||
runtimeState.rundown.plannedEnd =
|
runtimeState.rundown.plannedEnd =
|
||||||
rundownData.firstStart === null ? null : ((rundownData.firstStart + rundownData.totalDuration) as TimeOfDay);
|
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
||||||
|
|
||||||
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
||||||
}
|
}
|
||||||
@@ -297,7 +253,7 @@ export function load(
|
|||||||
|
|
||||||
// update state
|
// update state
|
||||||
runtimeState.timer.playback = Playback.Armed;
|
runtimeState.timer.playback = Playback.Armed;
|
||||||
runtimeState.timer.duration = event.duration as Duration;
|
runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
runtimeState.rundown.numEvents = metadata.timedEventOrder.length;
|
runtimeState.rundown.numEvents = metadata.timedEventOrder.length;
|
||||||
|
|
||||||
@@ -311,13 +267,13 @@ export function load(
|
|||||||
(firstStart === null || typeof firstStart === 'number') &&
|
(firstStart === null || typeof firstStart === 'number') &&
|
||||||
(startEpoch === null || typeof startEpoch === 'number')
|
(startEpoch === null || typeof startEpoch === 'number')
|
||||||
) {
|
) {
|
||||||
runtimeState.rundown.actualStart = firstStart as TimeOfDay;
|
runtimeState.rundown.actualStart = firstStart;
|
||||||
runtimeState._startEpoch = startEpoch;
|
runtimeState._startEpoch = startEpoch;
|
||||||
if (firstStart !== null && runtimeState.rundown.plannedStart !== null) {
|
if (firstStart !== null && runtimeState.rundown.plannedStart !== null) {
|
||||||
runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart);
|
runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart);
|
||||||
}
|
}
|
||||||
if (currentDay !== undefined) {
|
if (currentDay !== undefined) {
|
||||||
runtimeState.rundown.currentDay = currentDay as Day;
|
runtimeState.rundown.currentDay = currentDay;
|
||||||
}
|
}
|
||||||
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.offset.absolute = absolute;
|
runtimeState.offset.absolute = absolute;
|
||||||
@@ -409,16 +365,16 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
|
|||||||
|
|
||||||
// handle edge cases with roll
|
// handle edge cases with roll
|
||||||
if (runtimeState.timer.playback === Playback.Roll) {
|
if (runtimeState.timer.playback === Playback.Roll) {
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
// if waiting to roll, we update the targets and potentially start the timer
|
// if waiting to roll, we update the targets and potentially start the timer
|
||||||
if (runtimeState._timer.secondaryTarget !== null) {
|
if (runtimeState._timer.secondaryTarget !== null) {
|
||||||
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
|
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
|
||||||
// if the event is now, we queue a start
|
// if the event is now, we queue a start
|
||||||
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
|
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
} else {
|
} else {
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
}
|
}
|
||||||
@@ -435,7 +391,7 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
|
|||||||
|
|
||||||
runtimeState.timer.startedAt = null;
|
runtimeState.timer.startedAt = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
runtimeState.timer.addedTime = 0 as Duration;
|
runtimeState.timer.addedTime = 0;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
|
|
||||||
// this could be looked after by the timer
|
// this could be looked after by the timer
|
||||||
@@ -458,7 +414,7 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) {
|
|||||||
loadGroupFlagAndEnd(rundown, metadata, eventNowIndex);
|
loadGroupFlagAndEnd(rundown, metadata, eventNowIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(state: InternalRuntimeState = runtimeState): boolean {
|
export function start(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.eventNow === null) {
|
if (state.eventNow === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -466,19 +422,22 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state._now = timeCore.now();
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
|
||||||
|
state.clock = now;
|
||||||
state.timer.secondaryTimer = null;
|
state.timer.secondaryTimer = null;
|
||||||
|
|
||||||
// add paused time if it exists
|
// add paused time if it exists
|
||||||
if (state._timer.pausedAt) {
|
if (state._timer.pausedAt) {
|
||||||
const timeToAdd = (timeCore.toTimeOfDay(state._now) - state._timer.pausedAt) as Duration;
|
const timeToAdd = state.clock - state._timer.pausedAt;
|
||||||
state.timer.addedTime = timeCore.combineValues(state.timer.addedTime, timeToAdd);
|
state.timer.addedTime += timeToAdd;
|
||||||
state._timer.pausedDuration = timeCore.combineValues(state._timer.pausedDuration, timeToAdd);
|
state._timer.pausedDuration += timeToAdd;
|
||||||
state._timer.pausedAt = null;
|
state._timer.pausedAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.timer.startedAt === null) {
|
if (state.timer.startedAt === null) {
|
||||||
state.timer.startedAt = state._now;
|
state.timer.startedAt = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.timer.playback = Playback.Play;
|
state.timer.playback = Playback.Play;
|
||||||
@@ -486,15 +445,14 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
state.timer.elapsed = getElapsed(state);
|
state.timer.elapsed = getElapsed(state);
|
||||||
|
|
||||||
if (state.rundown.actualStart === null) {
|
if (state.rundown.actualStart === null) {
|
||||||
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, timeCore.toTimeOfDay(state._now)) +
|
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
|
||||||
state.eventNow.dayOffset) as Day;
|
|
||||||
state.rundown.currentDay = state._startDayOffset;
|
state.rundown.currentDay = state._startDayOffset;
|
||||||
state._startEpoch = state._now;
|
state._startEpoch = epoch;
|
||||||
state.rundown.actualStart = timeCore.toTimeOfDay(state._now);
|
state.rundown.actualStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
|
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
|
||||||
state.rundown.actualGroupStart = timeCore.toTimeOfDay(state._now);
|
state.rundown.actualGroupStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update timer phase
|
// update timer phase
|
||||||
@@ -517,18 +475,18 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pause(state: InternalRuntimeState = runtimeState): boolean {
|
export function pause(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.timer.playback !== Playback.Play) {
|
if (state.timer.playback !== Playback.Play) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.timer.playback = Playback.Pause;
|
state.timer.playback = Playback.Pause;
|
||||||
state._now = timeCore.now();
|
state.clock = timeCore.timeOfDayNow();
|
||||||
state._timer.pausedAt = timeCore.toTimeOfDay(state._now);
|
state._timer.pausedAt = state.clock;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(state: InternalRuntimeState = runtimeState): boolean {
|
export function stop(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.timer.playback === Playback.Stop) {
|
if (state.timer.playback === Playback.Stop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -558,7 +516,7 @@ export function addTime(amount: number) {
|
|||||||
|
|
||||||
if (willGoNegative && !runtimeState._timer.hasFinished) {
|
if (willGoNegative && !runtimeState._timer.hasFinished) {
|
||||||
// set finished time so side effects are triggered
|
// set finished time so side effects are triggered
|
||||||
runtimeState._timer.forceFinish = true;
|
runtimeState._timer.forceFinish = timeCore.timeOfDayNow();
|
||||||
} else {
|
} else {
|
||||||
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
||||||
if (willGoPositive) {
|
if (willGoPositive) {
|
||||||
@@ -567,8 +525,8 @@ export function addTime(amount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we can update the state after handling the side effects
|
// we can update the state after handling the side effects
|
||||||
runtimeState.timer.addedTime = timeCore.combineValues(runtimeState.timer.addedTime, amount as Duration);
|
runtimeState.timer.addedTime += amount;
|
||||||
runtimeState.timer.current = timeCore.combineValues(runtimeState.timer.current, amount as Duration);
|
runtimeState.timer.current += amount;
|
||||||
runtimeState.timer.elapsed = getElapsed(runtimeState);
|
runtimeState.timer.elapsed = getElapsed(runtimeState);
|
||||||
|
|
||||||
// update runtime delays: over - under
|
// update runtime delays: over - under
|
||||||
@@ -588,8 +546,10 @@ export type UpdateResult = {
|
|||||||
|
|
||||||
export function update(): UpdateResult {
|
export function update(): UpdateResult {
|
||||||
// 0. there are some things we always do
|
// 0. there are some things we always do
|
||||||
const previousClock = timeCore.toTimeOfDay(runtimeState._now);
|
const previousClock = runtimeState.clock;
|
||||||
runtimeState._now = timeCore.now(); // we update the clock on every update call
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
runtimeState.clock = now; // we update the clock on every update call
|
||||||
|
|
||||||
// 1. is playback idle?
|
// 1. is playback idle?
|
||||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||||
@@ -598,13 +558,13 @@ export function update(): UpdateResult {
|
|||||||
|
|
||||||
// calculate currentDay from epoch (days elapsed since playback was started)
|
// calculate currentDay from epoch (days elapsed since playback was started)
|
||||||
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
||||||
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
|
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
||||||
runtimeState.rundown.currentDay = timeCore.combineValues(runtimeState._startDayOffset, daysSinceStart);
|
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. are we waiting to roll?
|
// 2. are we waiting to roll?
|
||||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||||
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, timeCore.toTimeOfDay(runtimeState._now));
|
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
|
||||||
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,24 +618,22 @@ export function update(): UpdateResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// account for offset
|
// account for offset
|
||||||
const offsetClock = timeCore.toTimeOfDay(
|
const offsetClock = runtimeState.clock + runtimeState.offset.absolute;
|
||||||
timeCore.addDuration(runtimeState._now, runtimeState.offset.absolute as Duration),
|
|
||||||
);
|
|
||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
|
|
||||||
if (hasCrossedMidnight) {
|
if (hasCrossedMidnight) {
|
||||||
// if we crossed midnight, we need to update the target
|
// if we crossed midnight, we need to update the target
|
||||||
// this is the same logic from the roll function
|
// this is the same logic from the roll function
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget! - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock;
|
||||||
return {
|
return {
|
||||||
hasTimerFinished: false,
|
hasTimerFinished: false,
|
||||||
hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer! <= 0,
|
hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -692,7 +650,9 @@ export function roll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we will need to do some calculations, update the time first
|
// we will need to do some calculations, update the time first
|
||||||
runtimeState._now = timeCore.now();
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
runtimeState.clock = now;
|
||||||
|
|
||||||
// 2. if there is an event armed, we use it
|
// 2. if there is an event armed, we use it
|
||||||
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
|
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
|
||||||
@@ -704,9 +664,7 @@ export function roll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (offset) {
|
if (offset) {
|
||||||
runtimeState.offset = {
|
runtimeState.offset = { ...offset };
|
||||||
...offset,
|
|
||||||
} as InternalRuntimeState['offset'];
|
|
||||||
}
|
}
|
||||||
runtimeState.timer.playback = Playback.Roll;
|
runtimeState.timer.playback = Playback.Roll;
|
||||||
|
|
||||||
@@ -715,16 +673,15 @@ export function roll(
|
|||||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||||
? runtimeState.eventNow.timeEnd + dayInMs
|
? runtimeState.eventNow.timeEnd + dayInMs
|
||||||
: runtimeState.eventNow.timeEnd;
|
: runtimeState.eventNow.timeEnd;
|
||||||
runtimeState.timer.expectedFinish = normalisedEndTime as TimeOfDay;
|
runtimeState.timer.expectedFinish = normalisedEndTime;
|
||||||
|
|
||||||
// account for offset
|
// account for offset
|
||||||
// const offsetClock = timeCore.addDuration(runtimeState._now, -runtimeState.offset.absolute as Duration);
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
|
||||||
|
|
||||||
// state catch up
|
// state catch up
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
|
||||||
runtimeState.timer.current = runtimeState.timer.duration;
|
runtimeState.timer.current = runtimeState.timer.duration;
|
||||||
runtimeState.timer.elapsed = 0 as Duration;
|
runtimeState.timer.elapsed = 0;
|
||||||
|
|
||||||
// check if the event is ready to start or if needs to be pending
|
// check if the event is ready to start or if needs to be pending
|
||||||
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
|
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
|
||||||
@@ -733,8 +690,8 @@ export function roll(
|
|||||||
* If we are starting an event in roll mode
|
* If we are starting an event in roll mode
|
||||||
* we backtrace all the start times to the supposed start time of the event
|
* we backtrace all the start times to the supposed start time of the event
|
||||||
*/
|
*/
|
||||||
const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
|
const plannedStart = runtimeState.eventNow.timeStart;
|
||||||
runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
|
runtimeState.timer.startedAt = plannedStart;
|
||||||
// reset the secondary timer to cancel any countdowns
|
// reset the secondary timer to cancel any countdowns
|
||||||
runtimeState.timer.secondaryTimer = null;
|
runtimeState.timer.secondaryTimer = null;
|
||||||
|
|
||||||
@@ -752,20 +709,18 @@ export function roll(
|
|||||||
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
||||||
runtimeState.eventNow.dayOffset) as Day;
|
runtimeState.eventNow.dayOffset) as Day;
|
||||||
// backdate _startEpoch to when the event conceptually started
|
// backdate _startEpoch to when the event conceptually started
|
||||||
const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
|
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay);
|
||||||
runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
|
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration);
|
||||||
// calculate currentDay from the backdated epoch
|
// calculate currentDay from the backdated epoch
|
||||||
runtimeState.rundown.currentDay = timeCore.combineValues(
|
runtimeState.rundown.currentDay =
|
||||||
runtimeState._startDayOffset,
|
runtimeState._startDayOffset + timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
||||||
timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,9 +737,9 @@ export function roll(
|
|||||||
|
|
||||||
// account for offset but we only keep it if passed to us
|
// account for offset but we only keep it if passed to us
|
||||||
if (offset) {
|
if (offset) {
|
||||||
runtimeState.offset = { ...offset } as InternalRuntimeState['offset'];
|
runtimeState.offset = { ...offset };
|
||||||
}
|
}
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
|
|
||||||
const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
|
const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
|
||||||
|
|
||||||
@@ -811,7 +766,7 @@ export function roll(
|
|||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
// we need to normalise start time in case it is the day after
|
// we need to normalise start time in case it is the day after
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
|
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
|
|
||||||
// preload timer properties
|
// preload timer properties
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||||
@@ -834,18 +789,18 @@ export function roll(
|
|||||||
* when we start in roll mode
|
* when we start in roll mode
|
||||||
* we need to backtrace all times to the supposed start time of the event
|
* we need to backtrace all times to the supposed start time of the event
|
||||||
*/
|
*/
|
||||||
const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
|
const plannedStart = runtimeState.eventNow.timeStart;
|
||||||
const endTime =
|
const endTime =
|
||||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||||
? runtimeState.eventNow.timeEnd + dayInMs
|
? runtimeState.eventNow.timeEnd + dayInMs
|
||||||
: runtimeState.eventNow.timeEnd;
|
: runtimeState.eventNow.timeEnd;
|
||||||
runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
|
runtimeState.timer.startedAt = plannedStart;
|
||||||
runtimeState.timer.expectedFinish = endTime as TimeOfDay;
|
runtimeState.timer.expectedFinish = endTime;
|
||||||
|
|
||||||
// state catch up
|
// state catch up
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
runtimeState.timer.elapsed = 0 as Duration;
|
runtimeState.timer.elapsed = 0;
|
||||||
|
|
||||||
// update runtime
|
// update runtime
|
||||||
runtimeState.rundown.actualStart = plannedStart;
|
runtimeState.rundown.actualStart = plannedStart;
|
||||||
@@ -858,11 +813,11 @@ export function roll(
|
|||||||
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
||||||
runtimeState.eventNow.dayOffset) as Day;
|
runtimeState.eventNow.dayOffset) as Day;
|
||||||
// backdate _startEpoch to when the event conceptually started
|
// backdate _startEpoch to when the event conceptually started
|
||||||
const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
|
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay);
|
||||||
runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
|
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration);
|
||||||
// calculate currentDay from the backdated epoch
|
// calculate currentDay from the backdated epoch
|
||||||
runtimeState.rundown.currentDay = (runtimeState._startDayOffset +
|
runtimeState.rundown.currentDay = (runtimeState._startDayOffset +
|
||||||
timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now)) as Day;
|
timeCore.daysSinceStart(runtimeState._startEpoch, epoch)) as Day;
|
||||||
|
|
||||||
return { eventId: runtimeState.eventNow.id, didStart: true };
|
return { eventId: runtimeState.eventNow.id, didStart: true };
|
||||||
}
|
}
|
||||||
@@ -897,11 +852,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedGroupEnd = getExpectedEnd(
|
state.offset.expectedGroupEnd = getExpectedEnd(lastEvent, lastEventExpectedStart, state.rundown.currentDay!);
|
||||||
lastEvent,
|
|
||||||
lastEventExpectedStart,
|
|
||||||
state.rundown.currentDay!,
|
|
||||||
) as TimeOfDay;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,7 +869,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedFlagStart = expectedStart as TimeOfDay;
|
state.offset.expectedFlagStart = expectedStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,7 +884,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!) as TimeOfDay;
|
state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,13 +957,13 @@ export function loadGroupFlagAndEnd(
|
|||||||
if (!foundFlag && metadata.flags.includes(entry.id)) {
|
if (!foundFlag && metadata.flags.includes(entry.id)) {
|
||||||
foundFlag = true;
|
foundFlag = true;
|
||||||
state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list
|
state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list
|
||||||
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
|
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
|
||||||
foundGroupEnd = true;
|
foundGroupEnd = true;
|
||||||
state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1020,7 +971,7 @@ export function loadGroupFlagAndEnd(
|
|||||||
const lastID = playableEventOrder.at(-1);
|
const lastID = playableEventOrder.at(-1);
|
||||||
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
|
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
|
||||||
if (lastEvent) {
|
if (lastEvent) {
|
||||||
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Duration, OntimeEvent } from 'ontime-types';
|
import type { OntimeEvent } from 'ontime-types';
|
||||||
import { TimeStrategy } from 'ontime-types';
|
import { TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import { dayInMs } from '../date-utils/conversionUtils.js';
|
import { dayInMs } from '../date-utils/conversionUtils.js';
|
||||||
@@ -103,17 +103,16 @@ export function validateTimes(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description calculates event duration considering midnight
|
* @description calculates event duration considering midnight
|
||||||
* @deprecated - duration is the more useful value from the event data, prefer using that directly
|
|
||||||
* @param {number} timeStart
|
* @param {number} timeStart
|
||||||
* @param {number} timeEnd
|
* @param {number} timeEnd
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function calculateDuration(timeStart: number, timeEnd: number): Duration {
|
export function calculateDuration(timeStart: number, timeEnd: number): number {
|
||||||
// Durations must be positive
|
// Durations must be positive
|
||||||
if (timeEnd < timeStart) {
|
if (timeEnd < timeStart) {
|
||||||
return (timeEnd + dayInMs - timeStart) as Duration;
|
return timeEnd + dayInMs - timeStart;
|
||||||
}
|
}
|
||||||
return (timeEnd - timeStart) as Duration;
|
return timeEnd - timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateEnd(timeStart: number, duration: number): number {
|
export function calculateEnd(timeStart: number, duration: number): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user