Files
ontime/apps/server/src/setup/index.ts
T
Claude 05cf7c7c56 fix(report): measure events against the schedule they ran on
The runtime report joined live report data against the current rundown,
so editing an event after the show silently rewrote the report of a show
that had already happened. Durations, and the over/under chip in the
rundown, would change to match the edit.

Each event now records the schedule it actually ran on, and the report
is read from that.

- OntimeEventReport carries scheduledStart and scheduledDuration,
  captured when the event starts, plus a playCount so an event started
  twice no longer overwrites its own record without trace.
- The over/under calculation moves to ontime-utils, where the rundown
  chip and the settings panel share one implementation instead of
  deriving it separately and disagreeing.
- The report panel gains a summary of the show: planned against actual,
  drift, and how many events landed over, under or on time.
- Export CSV was rendering a trash bin icon.

No change to how or where anything is stored: the report stays in memory
exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
2026-08-09 19:19:02 +00:00

174 lines
6.0 KiB
TypeScript

/**
* This file handles resolving paths for the server resources
* There are two main directories
* - 1. the installation directory, exposed by __dirname
* - 2. the public directory, exposed by getAppDataPath()
*/
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { ensureDirectory } from '../utils/fileManagement.js';
import { config } from './config.js';
import { isProduction } from './environment.js';
/**
* Returns public path depending on OS
* This is the correct path for the app running in production mode
*/
export function getAppDataPath(): string {
/**
* If we are running in docker, the ONTIME_DATA environment variable
* allows moving the public directory to a user defined location
*/
if (process.env.ONTIME_DATA) {
return join(process.env.ONTIME_DATA);
}
switch (process.platform) {
case 'darwin': {
return join(process.env.HOME!, 'Library', 'Application Support', 'Ontime');
}
case 'win32': {
return join(process.env.APPDATA!, 'Ontime');
}
case 'linux': {
return join(process.env.HOME!, '.Ontime');
}
default: {
throw new Error('Could not resolve public folder for platform');
}
}
}
/**
* 1. Paths relative to the installation (or source in development)
* ------------------------------------------------------------------
* src/
* ├─ projects/
* ├─ external/
* │ ├─ demo/
* ├─ user/
* │ ├─ styles/
* │ │ ├─ override.css
* │ ├─ logo/
* │ │ ├─ logo.png
* │ ├─ translations/
* │ │ ├─ translations.json
*/
/** resolve file URL in both CJS and ESM (build and dev) */
if (import.meta.url) {
globalThis.__dirname = fileURLToPath(import.meta.url);
}
const currentDir = dirname(__dirname);
/**
* path to server src folder
* when running in dev, this file is located in src/setup, so we go one level up
* */
const srcDirectory = isProduction ? currentDir : join(currentDir, '../');
export const srcDir = {
root: srcDirectory,
/** Path to the react app */
clientDir: isProduction ? join(srcDirectory, 'client/') : join(srcDirectory, '../../client/build/'),
/** Path to the demo app */
demoDir: join(srcDirectory, 'external', config.demo),
} as const;
export const srcFiles = {
/** Path to start index.html */
clientIndexHtml: join(srcDir.clientDir, 'index.html'),
/** Path to bundled CSS */
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
/** Path to bundled translation */
translationsFile: join(srcDir.root, config.user, config.translations.directory, config.translations.filename),
/** Path to bundled external readme */
externalReadme: join(srcDir.root, config.external, 'README.md'),
/** Path to bundled user readme */
userReadme: join(srcDir.root, config.user, 'README.md'),
/** Path to bundled CSS readme */
cssReadme: join(srcDir.root, config.user, config.styles.directory, 'README.md'),
/** Path to bundled translation readme */
translationReadme: join(srcDir.root, config.user, config.translations.directory, 'README.md'),
/** Path to login */
login: join(srcDir.root, 'html/login.html'),
/** Path to legacy timer for old browsers */
timerLegacy: join(srcDir.root, 'html/timer-legacy.html'),
/** Path to ontime-logo */
logo: join(srcDir.root, config.user, config.logo, 'ontime-logo.png'),
};
/**
* 2. Paths relative to the user public directory
* ------------------------------------------------
*/
/** Resolve root to public directory */
const resolvePublicDirectory = getAppDataPath();
// Ensure directory tree is created
ensureDirectory(resolvePublicDirectory);
/**
* Path to external
* This is unique in the way that we use the src directory in development
* For simplicity we still bundle this in the public directory object
*/
const externalsStartDirectory = isProduction ? resolvePublicDirectory : join(srcDirectory, 'external');
export const publicDir = {
root: resolvePublicDirectory,
/** path to crash reports folder */
crashDir: join(resolvePublicDirectory, config.crash),
/** path to projects folder */
projectsDir: join(resolvePublicDirectory, config.projects),
/** path to corrupt folder */
corruptDir: join(resolvePublicDirectory, config.corrupt),
/** path to migrated folder */
migrateDir: join(resolvePublicDirectory, config.migrate),
/** path to uploads folder */
uploadsDir: join(resolvePublicDirectory, config.uploads),
/** path to external folder */
externalDir: join(
externalsStartDirectory,
isProduction ? config.external : '', // move to external folder in production
),
/** path to demo project folder */
demoDir: join(
externalsStartDirectory,
isProduction ? '/external/' : '', // move to external folder in production
config.demo,
),
/** path to user folder */
userDir: join(resolvePublicDirectory, config.user),
/** path to external styles override */
stylesDir: join(resolvePublicDirectory, config.user, config.styles.directory),
logoDir: join(resolvePublicDirectory, config.user, config.logo),
/** path to translations folder */
translationsDir: join(resolvePublicDirectory, config.user, config.translations.directory),
} as const;
/**
* Resolve path to specific files
*/
export const publicFiles = {
/** path to app state file */
appState: join(publicDir.root, config.appState),
/** path to restore file */
restoreFile: join(publicDir.root, config.restoreFile),
/** path to CSS override file */
cssOverride: join(publicDir.stylesDir, config.styles.filename),
/** path to custom translation file */
translationsFile: join(publicDir.translationsDir, config.translations.filename),
/** path to external readme file */
externalReadme: join(publicDir.externalDir, 'README.md'),
/** path to user readme file */
userReadme: join(publicDir.userDir, 'README.md'),
/** path to CSS readme file */
cssReadme: join(publicDir.stylesDir, 'README.md'),
/** path to translation readme file */
translationReadme: join(publicDir.translationsDir, 'README.md'),
} as const;