Compare commits

...

12 Commits

Author SHA1 Message Date
Carlos Valente 4c6acc4013 refactor: improve active styles for navigation buttons 2024-12-13 11:24:03 +01:00
Carlos Valente 26a24449de refactor: prevent modal close on click outside 2024-12-13 10:40:16 +01:00
Carlos Valente a1fb642441 bump version to 3.9.5 2024-12-13 10:40:16 +01:00
Carlos Valente b64b154330 refactor: disable shutdown 2024-12-13 10:39:58 +01:00
Carlos Valente 5032cbf65a refactor: prevent instantiating unavailable services 2024-12-13 10:39:58 +01:00
Carlos Valente 466360f9d1 chore: note service limitations 2024-12-13 10:39:58 +01:00
Carlos Valente ef2ea9a1da fix: recover edit menu and shortcuts 2024-12-13 10:39:45 +01:00
Carlos Valente fb83f48752 refactor: maintain an isOnline flag in client 2024-12-13 10:39:30 +01:00
Carlos Valente c1d53b0e55 refactor: improve empty state for project info 2024-12-13 10:12:06 +01:00
Alex Christoffer Rasmussen 7915cc822d Dockersafe rename (#1370)
* add dockerSafeRename function

* replace all rename functions

* add explanation to function
2024-12-10 23:27:51 +01:00
Carlos Valente 7dbf64d100 chore: restructure directory 2024-12-10 15:53:53 +01:00
Carlos Valente 6ffbf2af9d chore: move files to new structure 2024-12-10 15:53:53 +01:00
47 changed files with 260 additions and 125 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "3.9.4",
"version": "3.9.5",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "3.9.4",
"version": "3.9.5",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -19,7 +19,7 @@ import { ONTIME_VERSION } from './ONTIME_VERSION';
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
const Editor = React.lazy(() => import('./features/editors/ProtectedEditor'));
const Cuesheet = React.lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
const TimerView = React.lazy(() => import('./features/viewers/timer/Timer'));
+1 -1
View File
@@ -1,7 +1,7 @@
import axios, { AxiosResponse } from 'axios';
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
import { makeCSV, makeTable } from '../../views/cuesheet/cuesheet.utils';
import { apiEntryUrl } from './constants';
import { createBlob, downloadBlob } from './utils';
@@ -45,7 +45,7 @@ function NavigationMenu(props: NavigationMenuProps) {
const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure();
const { fullscreen, toggle } = useFullscreen();
const { toggleMirror } = useViewOptionsStore();
const { mirror, toggleMirror } = useViewOptionsStore();
const location = useLocation();
const menuRef = useRef<HTMLDivElement | null>(null);
@@ -65,7 +65,7 @@ function NavigationMenu(props: NavigationMenuProps) {
<DrawerBody padding={0}>
<div className={style.buttonsContainer}>
<div
className={style.link}
className={cx([style.link, fullscreen && style.current])}
tabIndex={0}
role='button'
onClick={toggle}
@@ -77,7 +77,7 @@ function NavigationMenu(props: NavigationMenuProps) {
{fullscreen ? <IoContract /> : <IoExpand />}
</div>
<div
className={style.link}
className={cx([style.link, mirror && style.current])}
tabIndex={0}
role='button'
onClick={() => toggleMirror()}
@@ -0,0 +1,20 @@
@use '../../../theme/viewerDefs' as *;
/* share the same style as a page layout */
.page {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 5rem;
}
@@ -0,0 +1,20 @@
import { CSSProperties } from 'react';
import Empty from './Empty';
import style from './EmptyPage.module.scss';
interface EmptyPageProps {
text?: string;
style?: CSSProperties;
}
export default function EmptyPage(props: EmptyPageProps) {
const { text, ...rest } = props;
return (
<div className={style.page}>
<Empty text={text} {...rest} />
</div>
);
}
+9 -3
View File
@@ -16,11 +16,17 @@ export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
/**
* Allows patching a property of the runtime store
* @param key
* @param value
*/
export function patchRuntime<K extends keyof RuntimeStore>(key: K, value: RuntimeStore[K]): void {
export function patchRuntimeProperty<K extends keyof RuntimeStore>(key: K, value: RuntimeStore[K]) {
const state = runtimeStore.getState();
state[key] = value;
runtimeStore.setState({ ...state });
}
/**
* Allows patching the entire runtime store
*/
export function patchRuntime(patch: Partial<RuntimeStore>) {
const state = runtimeStore.getState();
runtimeStore.setState({ ...state, ...patch });
}
+36 -21
View File
@@ -14,7 +14,7 @@ import {
} from '../stores/clientStore';
import { addDialog } from '../stores/dialogStore';
import { addLog } from '../stores/logger';
import { patchRuntime, runtimeStore } from '../stores/runtime';
import { patchRuntime, patchRuntimeProperty } from '../stores/runtime';
export let websocket: WebSocket | null = null;
let reconnectTimeout: NodeJS.Timeout | null = null;
@@ -39,12 +39,14 @@ export const connectSocket = () => {
}
socketSendJson('set-client-type', 'ontime');
socketSendJson('set-client-path', location.pathname + location.search);
setOnlineStatus(true);
};
websocket.onclose = () => {
console.warn('WebSocket disconnected');
setOnlineStatus(false);
if (shouldReconnect) {
reconnectTimeout = setTimeout(() => {
console.warn('WebSocket: attempting reconnect');
@@ -73,8 +75,8 @@ export const connectSocket = () => {
switch (type) {
case 'pong': {
const offset = (new Date().getTime() - new Date(payload).getTime()) * 0.5;
patchRuntime('ping', offset);
updateDevTools({ ping: offset }, ['PING']);
patchRuntimeProperty('ping', offset);
updateDevTools({ ping: offset });
break;
}
case 'client-id': {
@@ -131,64 +133,65 @@ export const connectSocket = () => {
break;
}
case 'ontime': {
runtimeStore.setState(payload as RuntimeStore);
if (!isProduction) {
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- removing the key from the payload
const { ping, ...serverPayload } = payload as Partial<RuntimeStore>;
patchRuntime(serverPayload);
updateDevTools(serverPayload);
break;
}
case 'ontime-clock': {
patchRuntime('clock', payload);
patchRuntimeProperty('clock', payload);
updateDevTools({ clock: payload });
break;
}
case 'ontime-timer': {
patchRuntime('timer', payload);
patchRuntimeProperty('timer', payload);
updateDevTools({ timer: payload });
break;
}
case 'ontime-onAir': {
patchRuntime('onAir', payload);
patchRuntimeProperty('onAir', payload);
updateDevTools({ onAir: payload });
break;
}
case 'ontime-message': {
patchRuntime('message', payload);
patchRuntimeProperty('message', payload);
updateDevTools({ message: payload });
break;
}
case 'ontime-runtime': {
patchRuntime('runtime', payload);
patchRuntimeProperty('runtime', payload);
updateDevTools({ runtime: payload });
break;
}
case 'ontime-eventNow': {
patchRuntime('eventNow', payload);
patchRuntimeProperty('eventNow', payload);
updateDevTools({ eventNow: payload });
break;
}
case 'ontime-currentBlock': {
patchRuntime('currentBlock', payload);
patchRuntimeProperty('currentBlock', payload);
updateDevTools({ currentBlock: payload });
break;
}
case 'ontime-publicEventNow': {
patchRuntime('publicEventNow', payload);
patchRuntimeProperty('publicEventNow', payload);
updateDevTools({ publicEventNow: payload });
break;
}
case 'ontime-eventNext': {
patchRuntime('eventNext', payload);
patchRuntimeProperty('eventNext', payload);
updateDevTools({ eventNext: payload });
break;
}
case 'ontime-publicEventNext': {
patchRuntime('publicEventNext', payload);
patchRuntimeProperty('publicEventNext', payload);
updateDevTools({ publicEventNext: payload });
break;
}
case 'ontime-auxtimer1': {
patchRuntime('auxtimer1', payload);
patchRuntimeProperty('auxtimer1', payload);
updateDevTools({ auxtimer1: payload });
break;
}
@@ -232,11 +235,23 @@ export const socketSendJson = (type: string, payload?: unknown) => {
);
};
function updateDevTools(newData: Partial<RuntimeStore>, store = RUNTIME) {
function updateDevTools(newData: Partial<RuntimeStore>) {
if (!isProduction) {
ontimeQueryClient.setQueryData(store, (oldData: RuntimeStore) => ({
ontimeQueryClient.setQueryData(RUNTIME, (oldData: RuntimeStore) => ({
...oldData,
...newData,
}));
}
}
/**
* Allows setting the status of the client
* We leverage the ping as an indication of the client's online status
* @example ping < 0 - client is offline
* @example ping > 0 -> client is online
*/
function setOnlineStatus(status: boolean) {
const derivedPing = status ? 1 : -1;
patchRuntimeProperty('ping', derivedPing);
updateDevTools({ ping: derivedPing });
}
@@ -10,6 +10,7 @@ import { maybeAxiosError } from '../../../../common/api/utils';
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
import { isKeyEscape } from '../../../../common/utils/keyEvent';
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
import { isOntimeCloud } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import { cycles } from './integrationUtils';
@@ -106,6 +107,9 @@ export default function OscIntegrations() {
</Button>
</div>
</Panel.SubHeader>
{isOntimeCloud && (
<Panel.Highlight>For security reasons OSC integrations are not available in the cloud service.</Panel.Highlight>
)}
<Panel.Divider />
@@ -11,7 +11,7 @@ import {
} from '@chakra-ui/react';
import { useElectronEvent } from '../../../../common/hooks/useElectronEvent';
import { isLocalhost } from '../../../../externals';
import { isLocalhost, isOntimeCloud } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
export default function ShutdownPanel() {
@@ -24,18 +24,28 @@ export default function ShutdownPanel() {
onClose();
};
const canShutdown = isElectron || isLocalhost;
return (
<>
<Panel.Header>Shutdown Ontime</Panel.Header>
<Panel.Section>
<Panel.Paragraph>
This will shutdown the Ontime server. <br />
The runtime state will be lost, but your project is kept for next time.
</Panel.Paragraph>
{isOntimeCloud ? (
<Panel.Highlight>
For security reasons, shutting down the server must be done from the Ontime Cloud dashboard.
</Panel.Highlight>
) : (
<Panel.Paragraph>
This will shutdown the Ontime server. <br />
The runtime state will be lost, but your project is kept for next time.
</Panel.Paragraph>
)}
<Button colorScheme='red' onClick={onOpen} maxWidth='350px' isDisabled={!(isElectron || isLocalhost)}>
Shutdown ontime
</Button>
<Panel.Description>Note: Ontime can only be shutdown from the machine it is running in.</Panel.Description>
{!canShutdown && (
<Panel.Description>Note: Ontime can only be shutdown from the machine it is running in.</Panel.Description>
)}
<AlertDialog variant='ontime' isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
@@ -49,7 +59,7 @@ export default function ShutdownPanel() {
<Button ref={cancelRef} onClick={onClose} variant='ontime-ghosted-white'>
Cancel
</Button>
<Button colorScheme='red' onClick={sendShutdown} ml={4}>
<Button colorScheme='red' onClick={sendShutdown} disabled={!canShutdown}>
Shutdown
</Button>
</AlertDialogFooter>
@@ -1,41 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`makeTable() > returns array of arrays with given fields 1`] = `
[
[
"Ontime · Rundown export",
],
[
"Project title: test title",
],
[
"Project description: test description",
],
[
"Time Start",
"Time End",
"Duration",
"ID",
"Colour",
"Cue",
"Title",
"Note",
"Is Public? (x)",
"Skip?",
"lighting",
],
[
"00:00:00",
"00:00:00",
"...",
"",
"",
"",
"test title 1",
"",
"x",
"",
"",
],
]
`;
@@ -53,7 +53,7 @@ export default function Welcome(props: WelcomeProps) {
};
return (
<Modal isOpen onClose={handleClose} variant='ontime'>
<Modal isOpen onClose={handleClose} closeOnOverlayClick={false} variant='ontime'>
<ModalOverlay />
<ModalContent maxWidth='max(640px, 40vw)'>
<ModalCloseButton />
@@ -11,7 +11,7 @@ import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
import DelayRow from './cuesheet-table-elements/DelayRow';
import EventRow from './cuesheet-table-elements/EventRow';
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
import { useCuesheetSettings } from './store/CuesheetSettings';
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
import useColumnManager from './useColumnManager';
import style from './Cuesheet.module.scss';
@@ -11,16 +11,16 @@ import { useCuesheet } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import { useFlatRundown } from '../../common/hooks-query/useRundown';
import { CuesheetOverview } from '../overview/Overview';
import { CuesheetOverview } from '../../features/overview/Overview';
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
import { useCuesheetSettings } from './store/CuesheetSettings';
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
import Cuesheet from './Cuesheet';
import { makeCuesheetColumns } from './cuesheetCols';
import styles from './CuesheetWrapper.module.scss';
import styles from './CuesheetPage.module.scss';
export default function CuesheetWrapper() {
export default function CuesheetPage() {
// TODO: can we use the normalised rundown for the table?
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
const { data: customFields } = useCustomFields();
@@ -1,11 +1,11 @@
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
import CuesheetWrapper from './CuesheetWrapper';
import CuesheetPage from './CuesheetPage';
export default function ProtectedCuesheet() {
return (
<ProtectRoute permission='operator'>
<CuesheetWrapper />
<CuesheetPage />
</ProtectRoute>
);
}
@@ -1,4 +1,6 @@
import { makeCSV, makeTable, parseField } from '../cuesheetUtils';
import { ProjectData } from 'ontime-types';
import { makeCSV, makeTable, parseField } from '../cuesheet.utils';
describe('parseField()', () => {
it('returns a string from given millis on timeStart, TimeEnd and duration', () => {
@@ -27,6 +29,7 @@ describe('parseField()', () => {
});
it('returns an empty string on undefined fields', () => {
// @ts-expect-error -- testing user data with missing fields
expect(parseField('title')).toBe('');
});
@@ -51,6 +54,7 @@ describe('makeTable()', () => {
const headerData = {
title: 'test title',
description: 'test description',
projectLogo: 'test logo',
};
const tableData = [
{
@@ -66,8 +70,48 @@ describe('makeTable()', () => {
lighting: { label: 'test' },
};
const table = makeTable(headerData, tableData, customFields);
expect(table).toMatchSnapshot();
// @ts-expect-error -- testing user data with missing fields
const table = makeTable(headerData as ProjectData, tableData, customFields);
expect(table).not.toContain('test logo');
expect(table).toMatchInlineSnapshot(`
[
[
"Ontime · Rundown export",
],
[
"Project title: test title",
],
[
"Project description: test description",
],
[
"Time Start",
"Time End",
"Duration",
"ID",
"Colour",
"Cue",
"Title",
"Note",
"Is Public? (x)",
"Skip?",
"lighting",
],
[
"00:00:00",
"00:00:00",
"...",
"",
"",
"",
"test title 1",
"",
"x",
"",
"",
],
]
`);
});
});
@@ -6,7 +6,7 @@ import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon'
import useProjectData from '../../../common/hooks-query/useProjectData';
import { cx, enDash } from '../../../common/utils/styleUtils';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { useCuesheetSettings } from '../store/CuesheetSettings';
import { useCuesheetSettings } from '../store/cuesheetSettingsStore';
import CuesheetTableHeaderTimers from './CuesheetTableHeaderTimers';
@@ -1,6 +1,6 @@
import { useClock, useTimer } from '../../../common/hooks/useSocket';
import ClockTime from '../../viewers/common/clock-time/ClockTime';
import RunningTime from '../../viewers/common/running-time/RunningTime';
import ClockTime from '../../../features/viewers/common/clock-time/ClockTime';
import RunningTime from '../../../features/viewers/common/running-time/RunningTime';
import style from './CuesheetTableHeader.module.scss';
@@ -3,7 +3,7 @@ import { Button, Checkbox, Switch } from '@chakra-ui/react';
import { Column } from '@tanstack/react-table';
import { OntimeRundownEntry } from 'ontime-types';
import { useCuesheetSettings } from '../store/CuesheetSettings';
import { useCuesheetSettings } from '../store/cuesheetSettingsStore';
import style from './CuesheetTableSettings.module.scss';
@@ -4,10 +4,10 @@ import { CellContext, ColumnDef } from '@tanstack/react-table';
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
import DelayIndicator from '../../common/components/delay-indicator/DelayIndicator';
import RunningTime from '../viewers/common/running-time/RunningTime';
import RunningTime from '../../features/viewers/common/running-time/RunningTime';
import EditableCell from './cuesheet-table-elements/EditableCell';
import { useCuesheetSettings } from './store/CuesheetSettings';
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
import style from './Cuesheet.module.scss';
@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { booleanFromLocalStorage } from '../../../common/utils/localStorage';
interface CuesheetSettings {
interface CuesheetSettingsStore {
showSettings: boolean;
showIndexColumn: boolean;
followSelected: boolean;
@@ -36,7 +36,7 @@ enum CuesheetKeys {
Seconds = 'ontime-cuesheet-hide-sceconds',
}
export const useCuesheetSettings = create<CuesheetSettings>()((set) => ({
export const useCuesheetSettings = create<CuesheetSettingsStore>()((set) => ({
showSettings: false,
showIndexColumn: booleanFromLocalStorage(CuesheetKeys.ColumnIndex, true),
followSelected: booleanFromLocalStorage(CuesheetKeys.Follow, false),
@@ -1,6 +1,7 @@
import { ProjectData } from 'ontime-types';
import Empty from '../../common/components/state/Empty';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
@@ -16,7 +17,7 @@ interface ProjectInfoProps {
isMirrored: boolean;
}
export default function ProjectInfoProps(props: ProjectInfoProps) {
export default function ProjectInfo(props: ProjectInfoProps) {
const { general, isMirrored } = props;
useWindowTitle('Project info');
@@ -25,6 +26,25 @@ export default function ProjectInfoProps(props: ProjectInfoProps) {
return <Empty text='No data found' />;
}
if (!general) {
return (
<>
<ViewParamsEditor viewOptions={projectInfoOptions} />
return <EmptyPage text='No data found' />;
</>
);
}
const isEmpty = Object.values(general).every((value) => !value);
if (isEmpty) {
return (
<>
<ViewParamsEditor viewOptions={projectInfoOptions} />
<EmptyPage text='The project has no data yet' />;
</>
);
}
return (
<div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'>
<ViewParamsEditor viewOptions={projectInfoOptions} />
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "3.9.4",
"version": "3.9.5",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+18 -1
View File
@@ -29,6 +29,7 @@ function getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, sho
const template = [
...(isMac ? [makeMacMenu(askToQuit)] : []),
makeFileMenu(serverUrl, redirectWindow, showDialog, download),
makeEditMenu(),
makeViewMenu(clientUrl),
makeSettingsMenu(redirectWindow),
makeHelpMenu(redirectWindow),
@@ -61,6 +62,22 @@ function makeMacMenu(askToQuit) {
};
}
/**
* Utility function generates the edit menu
* @returns {Object}
*/
function makeEditMenu() {
return {
label: 'Edit',
submenu: [
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
],
};
}
/**
* Utility function generates the file menu
* @param {string} serverUrl - base url for the application
@@ -253,7 +270,7 @@ function makeSettingsMenu(redirectWindow) {
click: () => redirectWindow('/editor?settings=network__log'),
},
{
label: 'Manage cleints',
label: 'Manage clients',
click: () => redirectWindow('/editor?settings=network__clients'),
},
],
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "3.9.4",
"version": "3.9.5",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+16 -11
View File
@@ -10,7 +10,7 @@ import { extname } from 'node:path';
// import utils
import { publicDir, srcDir, srcFiles } from './setup/index.js';
import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { environment, isOntimeCloud, isProduction, updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -249,16 +249,6 @@ export const startIntegrations = async () => {
// if a config is not provided, we use the persisted one
const { osc, http } = getDataProvider().getData();
if (osc) {
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
try {
oscIntegration.init(osc);
integrationService.register(oscIntegration);
} catch (error) {
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
}
}
if (http) {
logger.info(LogOrigin.Tx, 'Initialising HTTP Integration...');
try {
@@ -268,6 +258,21 @@ export const startIntegrations = async () => {
logger.error(LogOrigin.Tx, `HTTP Integration initialisation failed: ${error}`);
}
}
if (isOntimeCloud) {
logger.info(LogOrigin.Tx, 'Skipping OSC in Cloud environment...');
return;
}
if (osc) {
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
try {
oscIntegration.init(osc);
integrationService.register(oscIntegration);
} catch (error) {
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
}
}
};
/**
+1 -1
View File
@@ -14,7 +14,7 @@ export const isTest = Boolean(process.env.IS_TEST);
export const environment = isTest ? 'test' : env;
export const isDocker = env === 'docker';
export const isProduction = isDocker || (env === 'production' && !isTest);
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
/**
* Updates the router prefix in the index.html file
* This is only needed in the cloud environment where the client is not at the root segment
@@ -1,12 +1,13 @@
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import { copyFile, rename } from 'fs/promises';
import { copyFile } from 'fs/promises';
import { logger } from '../../classes/Logger.js';
import { publicDir } from '../../setup/index.js';
import {
appendToName,
dockerSafeRename,
ensureDirectory,
generateUniqueFileName,
getFileNameFromPath,
@@ -93,7 +94,7 @@ async function handleCorruptedFile(filePath: string, fileName: string): Promise<
// and make a new file with the recovered data
const newPath = appendToName(filePath, '(recovered)');
await rename(filePath, newPath);
await dockerSafeRename(filePath, newPath);
return getFileNameFromPath(newPath);
}
@@ -231,7 +232,7 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
}
const pathToRenamed = getPathToProject(newFilename);
await rename(projectFilePath, pathToRenamed);
await dockerSafeRename(projectFilePath, pathToRenamed);
// Update the last loaded project config if current loaded project is the one being renamed
const isLoaded = await isLastLoadedProject(originalFile);
@@ -1,11 +1,16 @@
import { DatabaseModel, MaybeString, ProjectFile } from 'ontime-types';
import { existsSync } from 'fs';
import { copyFile, readFile, rename, stat } from 'fs/promises';
import { copyFile, readFile, stat } from 'fs/promises';
import { extname, join } from 'path';
import { publicDir } from '../../setup/index.js';
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
import {
dockerSafeRename,
ensureDirectory,
getFilesFromFolder,
removeFileExtension,
} from '../../utils/fileManagement.js';
/**
* Handles the upload of a new project file
@@ -14,13 +19,13 @@ import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../
*/
export async function handleUploaded(filePath: string, name: string) {
const newFilePath = join(publicDir.projectsDir, name);
await rename(filePath, newFilePath);
await dockerSafeRename(filePath, newFilePath);
}
export async function handleImageUpload(filePath: string, name: string): Promise<string> {
ensureDirectory(publicDir.logoDir);
const newFilePath = join(publicDir.logoDir, name);
await rename(filePath, newFilePath);
await dockerSafeRename(filePath, newFilePath);
return name;
}
@@ -86,7 +91,7 @@ export async function copyCorruptFile(filePath: string, name: string): Promise<v
*/
export async function moveCorruptFile(filePath: string, name: string): Promise<void> {
const newPath = join(publicDir.corruptDir, name);
return rename(filePath, newPath);
return dockerSafeRename(filePath, newPath);
}
/**
+11 -2
View File
@@ -1,5 +1,5 @@
import { existsSync, mkdirSync } from 'fs';
import { readdir, copyFile } from 'fs/promises';
import { existsSync, mkdirSync, PathLike } from 'fs';
import { readdir, copyFile, unlink } from 'fs/promises';
import { basename, extname, join, parse } from 'path';
/**
@@ -105,3 +105,12 @@ export async function copyDirectory(src: string, dest: string) {
}
}
}
/**
* workaround avoids origin errors in docker deployments
* EXDEV cross-device link not permitted
*/
export async function dockerSafeRename(oldPath: PathLike, newPath: PathLike) {
await copyFile(oldPath, newPath);
await unlink(oldPath);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "3.9.4",
"version": "3.9.5",
"description": "Time keeping for live events",
"keywords": [
"ontime",