Compare commits

...

5 Commits

Author SHA1 Message Date
Claude a6a4d5d89e feat(ui): play a sound when the timer ends
Adds an opt-in "Play sound on timer end" option to the timer view, which
sounds the bundled buzzer when the timer transitions into overtime.

Browsers reject audio playback until the document has been interacted
with, and that permission is lost on every page load. Since a timer
screen is typically left unattended this would fail silently, so the
audio element is primed on the first interaction with the page and the
view shows a hint while it is still blocked.

The hint is tied to mouse movement, which does not itself grant playback
permission, so it stays off screen unless somebody is at the machine to
act on it.

Off by default, and inert when off: no audio element is created and no
listeners are registered. The change is contained to the timer view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np5ZL4TnZSutqA1Lqzm5Gq
2026-08-26 12:34:31 +00:00
Carlos Valente 1ddfe8a969 wip: poc for playing a sound on time end 2026-08-26 12:29:51 +00:00
Carlos Valente b9683f00dc fix: prevent stale rundown cache on rename 2026-08-25 21:41:34 +02:00
Carlos Valente 63fdc50c50 docs(llms): document ongoing code migrations 2026-08-25 21:41:18 +02:00
Carlos Valente 5acfc4a122 docs(llms): setup agents documentation 2026-08-25 21:41:18 +02:00
19 changed files with 735 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
---
name: code-review
description: Review Ontime changes for concrete correctness, architecture, testing, routing, security, and maintenance issues. For GitHub Copilot review.
---
# Review Ontime changes
Review as a maintainer. Find material defects, not speculative advice, style preferences, or generic checklists. Approve when no material issue remains.
## Load relevant context
Read the full diff, PR description, linked issue, tests, and nearby owning code. Then load only applicable guides:
- Every non-trivial review: [change assessment](../../../docs/agent-guides/change-assessment.md)
- Module placement, server layers, client state, shared packages: [architecture](../../../docs/agent-guides/architecture.md)
- Tests or changed behaviour: [testing](../../../docs/agent-guides/testing.md)
- Comments, abstractions, naming, complexity: [code quality](../../../docs/agent-guides/code-quality.md)
- Authentication, external input, files, integrations, assets, secrets: [security](../../../docs/agent-guides/security.md)
- Routes, URLs, websockets, authentication, cookies, presets, assets: [routing and cloud](../../../docs/agent-guides/routing-and-cloud.md)
- Rundowns, timers, persistence, imports, cache, realtime: [domain invariants](../../../docs/agent-guides/domain-invariants.md)
- Commands, imports, dependencies, formatting, CI claims: [workflow](../../../docs/agent-guides/workflow.md)
Check a guide and nearby canonical code before citing an Ontime convention. Skip unrelated guides.
## Review order
1. Establish intent and affected runtime surfaces.
2. Read tests first. Identify claimed behaviour and coverage.
3. Trace implementation, errors, and state transitions.
4. Check correctness/data integrity, security, architecture/testability, cloud routing, lifecycle/performance, maintainability.
5. Verify claimed checks. Never claim unobserved results.
Passing tests do not prove architecture, routing, comments, or error paths. Review changed behaviour only; include existing problems only when the diff worsens or relies on them.
## Findings
Report only concrete, actionable issues. Each finding: tight line range, direct defect, impact and trigger, smallest viable remedy when unclear.
- **P0 — Critical:** data loss, exploitable vulnerability, broadly broken production. Blocks merge.
- **P1 — High:** likely correctness failure or major supported-deployment regression. Blocks merge.
- **P2 — Medium:** real edge-case defect, architecture regression, missing business-rule test, stale comment, meaningful maintenance risk. Normally blocks merge.
- **P3 — Low:** local improvement with limited impact. No subjective style or tool-managed formatting.
Order by priority. Prefer few high-confidence findings. No praise or checklist before findings. If none, say so and note verification gaps or residual risk.
Then give one concise PR-level value/risk/complexity assessment. Never repeat it per finding.
+22
View File
@@ -0,0 +1,22 @@
# Ontime agent guide
Make the smallest maintainable change. Keep scope narrow. Inspect nearby code first. Reuse helpers and boundaries when semantics match.
## Load only relevant guides
- Commands, validation, formatting, imports, PRs: [workflow](docs/agent-guides/workflow.md).
- Non-trivial planning, implementation, review: [change assessment](docs/agent-guides/change-assessment.md).
- Module placement, server layers, client state, shared packages: [architecture](docs/agent-guides/architecture.md).
- Tests or changed behaviour: [testing](docs/agent-guides/testing.md).
- Comments, naming, abstractions, maintainability: [code quality](docs/agent-guides/code-quality.md).
- Authentication, external input, files, integrations, assets, secrets: [security](docs/agent-guides/security.md).
- Navigation, URLs, API paths, websockets, redirects, cookies, static assets: [routing and cloud](docs/agent-guides/routing-and-cloud.md).
- Rundowns, timers, imports, persistence, cache, websockets: [domain invariants](docs/agent-guides/domain-invariants.md).
Load multiple guides when needed. Skip unrelated guides for mechanical work.
## Before handoff
- Check the final diff for scope, stale comments, temporary code, redundant tests, generated files.
- If work reveals a missing, stable, reusable system, domain, or product invariant, update its owning guide. Exclude guesses, one-off bugs, and implementation details.
- Follow [workflow verification](docs/agent-guides/workflow.md). Report only observed results.
Binary file not shown.
+19
View File
@@ -195,6 +195,25 @@
font-weight: 600;
}
/* =================== SOUND PROMPT ===================*/
.sound-prompt {
position: absolute;
bottom: $view-block-padding;
left: $view-inline-padding;
padding: 0.5em 0.75em;
border-radius: $element-border-radius;
background-color: $viewer-card-bg-color;
color: $viewer-secondary-color;
font-size: $timer-label-size;
text-transform: uppercase;
pointer-events: none;
transition: opacity $viewer-transition-time;
&--hidden {
opacity: 0;
}
}
/* =================== LOGO ===================*/
.logo {
position: absolute;
+23
View File
@@ -8,6 +8,7 @@ import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivity';
import { useTimerSocket } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { cx } from '../../common/utils/styleUtils';
@@ -30,6 +31,7 @@ import {
getTotalTime,
} from './timer.utils';
import { TimerData, useTimerData } from './useTimerData';
import { useTimerSound } from './useTimerSound';
import './Timer.scss';
@@ -66,6 +68,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
freezeOvertime,
freezeMessage,
hidePhase,
endSound,
font,
keyColour,
timerColour,
@@ -75,6 +78,8 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
const { getLocalizedString } = useTranslation();
const localisedMinutes = getLocalizedString('common.minutes');
const { showPrompt } = useTimerSound(time.phase, endSound);
// gather modifiers
const viewTimerType = timerType ?? timerTypeNow;
const showOverlay = getShowMessage(message.timer);
@@ -156,6 +161,8 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
<ViewParamsEditor target={OntimeView.Timer} viewOptions={timerOptions} />
{showPrompt && <EnableSoundPrompt />}
<div className={cx(['blackout', message.timer.blackout && 'blackout--active'])} />
{!hideMessage && (
@@ -227,3 +234,19 @@ function TimerAutoTickingClock({ clockFormat }: { clockFormat: MaybeString }) {
</div>
);
}
/**
* Nudges the user to interact with the screen so that the browser allows audio playback
* Any interaction arms the sound, so this is a hint rather than a control
* It is tied to mouse movement since that does not itself grant playback permission,
* which keeps the hint off screen unless somebody is at the machine to act on it
*/
function EnableSoundPrompt() {
const isUserActive = useFadeOutOnInactivity(true);
return (
<div className={cx(['sound-prompt', !isUserActive && 'sound-prompt--hidden'])} aria-live='polite'>
Tap the screen to enable sound
</div>
);
}
@@ -0,0 +1,34 @@
import { TimerPhase } from 'ontime-types';
import { shouldPlayEndSound } from '../timer.utils';
describe('shouldPlayEndSound()', () => {
test.each([TimerPhase.Default, TimerPhase.Warning, TimerPhase.Danger])(
'sounds when a running timer goes into overtime from %s',
(previousPhase) => {
expect(shouldPlayEndSound(previousPhase, TimerPhase.Overtime)).toBe(true);
},
);
it('stays silent on the first phase we see, a client could be joining mid-overtime', () => {
expect(shouldPlayEndSound(null, TimerPhase.Overtime)).toBe(false);
});
it('stays silent when the phase was reset, a reload during overtime starts from none', () => {
expect(shouldPlayEndSound(TimerPhase.None, TimerPhase.Overtime)).toBe(false);
});
it('stays silent for a roll timer waiting to start', () => {
expect(shouldPlayEndSound(TimerPhase.Pending, TimerPhase.Overtime)).toBe(false);
});
it('sounds once, not on every update while in overtime', () => {
expect(shouldPlayEndSound(TimerPhase.Overtime, TimerPhase.Overtime)).toBe(false);
});
it('stays silent on phases which are not the end of the timer', () => {
expect(shouldPlayEndSound(TimerPhase.Default, TimerPhase.Warning)).toBe(false);
expect(shouldPlayEndSound(TimerPhase.Warning, TimerPhase.Danger)).toBe(false);
expect(shouldPlayEndSound(TimerPhase.Overtime, TimerPhase.None)).toBe(false);
});
});
@@ -76,6 +76,14 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
type: 'boolean',
defaultValue: false,
},
{
id: 'endSound',
title: 'Play sound on timer end',
description:
'Plays a sound in this screen when the timer reaches zero. The screen must be interacted with once before it can play',
type: 'boolean',
defaultValue: false,
},
],
},
{
@@ -193,6 +201,7 @@ type TimerOptions = {
freezeOvertime: boolean;
freezeMessage: string;
hidePhase: boolean;
endSound: boolean;
font?: string;
keyColour?: string;
timerColour?: string;
@@ -227,6 +236,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
freezeOvertime: isStringBoolean(getValue('freezeOvertime')),
freezeMessage: getValue('freezeMessage') ?? '',
hidePhase: isStringBoolean(getValue('hidePhase')),
endSound: isStringBoolean(getValue('endSound')),
font: getValue('font') ?? undefined,
keyColour: makeColourString(getValue('keyColour')),
@@ -189,3 +189,18 @@ export function getCardData(
nextSecondary,
};
}
/**
* Whether the end of timer sound should play for a given phase transition
* We only sound the transition into overtime from a phase that was already counting,
* which keeps a client that connects or reloads mid-overtime silent
*/
export function shouldPlayEndSound(previousPhase: TimerPhase | null, phase: TimerPhase): boolean {
if (phase !== TimerPhase.Overtime) {
return false;
}
return (
previousPhase === TimerPhase.Default || previousPhase === TimerPhase.Warning || previousPhase === TimerPhase.Danger
);
}
@@ -0,0 +1,77 @@
import { TimerPhase } from 'ontime-types';
import { useEffect, useRef, useState } from 'react';
import buzzer from '../../assets/sounds/buzzer.mp3';
import { shouldPlayEndSound } from './timer.utils';
/**
* Plays a sound when the timer reaches its end
*
* Browsers reject playback until the document has been interacted with, and that permission
* is lost on every page load. Since a timer screen is typically left unattended, we prime the
* audio element on the first interaction and let the view prompt for one if it never comes.
* Safari grants the permission per element, so priming has to call play() on this element from
* inside the event handler, it is not enough to know that an interaction happened.
*/
export function useTimerSound(phase: TimerPhase, enabled: boolean): { showPrompt: boolean } {
const audioRef = useRef<HTMLAudioElement | null>(null);
const previousPhaseRef = useRef<TimerPhase | null>(null);
const [isArmed, setIsArmed] = useState(false);
useEffect(() => {
if (!enabled) {
return;
}
audioRef.current = new Audio(buzzer);
return () => {
audioRef.current?.pause();
audioRef.current = null;
setIsArmed(false);
};
}, [enabled]);
useEffect(() => {
if (!enabled || isArmed) {
return;
}
const controller = new AbortController();
const prime = () => {
audioRef.current
?.play()
.then(() => {
if (!audioRef.current) return;
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsArmed(true);
})
.catch(() => {
// playback is still blocked, a later interaction will try again
});
};
document.addEventListener('pointerdown', prime, { capture: true, signal: controller.signal });
document.addEventListener('keydown', prime, { capture: true, signal: controller.signal });
return () => {
controller.abort();
};
}, [enabled, isArmed]);
useEffect(() => {
const previousPhase = previousPhaseRef.current;
previousPhaseRef.current = phase;
if (!enabled || !shouldPlayEndSound(previousPhase, phase)) {
return;
}
audioRef.current?.play().catch(() => {
// the screen has not been interacted with, the view shows a prompt for it
});
}, [enabled, phase]);
return { showPrompt: enabled && !isArmed };
}
@@ -818,7 +818,7 @@ export async function renameRundown(id: string, title: string) {
const dataProvider = getDataProvider();
const rundown = dataProvider.getRundown(id);
await dataProvider.setRundown(id, { ...rundown, title });
await dataProvider.setRundown(id, { ...rundown, title, revision: rundown.revision + 1 });
/**
* If we are modifying the loaded rundown we re-init it
+85
View File
@@ -0,0 +1,85 @@
# Ontime architecture
Use for module placement or cross-boundary changes.
## Direction
Dependencies point toward pure domain logic:
```text
HTTP request
-> validation / router or controller
-> service orchestration
-> pure domain utilities
service orchestration
-> DAO / stores / adapters / external clients
```
Keep a layer only for clearer ownership, isolated side effects, or direct business-rule tests.
## Reuse and ownership
Before adding a module, helper, service, or interface, find the concept owner and inspect callers.
1. Reuse when the contract already matches.
2. Extend for the same concept when the contract stays coherent.
3. Keep local when reuse needs flags, broad optional inputs, unrelated modes, or leaky terms.
4. Generalise only after stable common behaviour appears across real callers.
Do not duplicate canonical rules or distort an abstraction to force reuse. Small local duplication can beat coupling unrelated concepts.
## Server
### Routers and controllers
Routers declare paths and middleware. Controllers map validated HTTP input to typed service arguments, then results/errors to responses.
Keep reusable calculations, domain decisions, transformations, persistence workflows, and integration coordination out of handlers. Simple reads may stay direct when a service would only pass through.
### Services
Services orchestrate use cases and side-effect boundaries. Make ordering and effects visible. Move substantial branching, calculation, comparison, parsing, and transformation to pure utilities.
### Pure utilities
Pass required state, config, and time explicitly. No I/O, stores/globals, logging, websocket publication, browser inspection, or caller-owned mutation unless explicitly contracted.
Use focused Vitest coverage. Colocate feature logic. Move to `ontime-utils` only for genuine cross-package use.
### State and boundaries
- DAOs/data providers: persistence.
- Stores: mutable runtime state.
- Adapters/clients: external protocols and integrations.
- Validators/parsers: protect boundaries before domain logic.
- Commit state before dependent notifications or invalidations.
Old layering exceptions are context, not precedent. Improve touched boundaries only through focused, behaviour-preserving moves.
## Client
- `common/api`: HTTP transport.
- `common/hooks-query`: TanStack Query reads, mutations, keys, cache, invalidation.
- `features`: reusable product capabilities and domain behaviour.
- `views`: route-level composition.
- `common`: genuinely cross-feature code.
Keep substantial rules out of JSX, effects, and handlers. Use tested, colocated pure utilities. TanStack Query owns server state; established Zustand/context owns local state. No parallel caches.
Limit subscriptions with selectors. Keep effect dependencies stable. Clean up listeners, intervals, external resources.
## Shared packages
- `ontime-types`: shared contracts; type-focused.
- `ontime-utils`: environment-independent, side-effect-free shared logic.
- Never import application layers into shared packages.
- Keep feature-specific helpers with their owner, even when used by another file.
## Review prompts
- Business rule understandable/testable without app startup?
- Transport, orchestration, state, transformation separated?
- Existing owner reused without forcing unrelated behaviour?
- Abstraction removes concepts rather than relocating them?
- Smallest focused remedy clear?
+30
View File
@@ -0,0 +1,30 @@
# Change assessment
Use before non-trivial planning/implementation and during non-trivial review. Let it shape scope and verification; avoid process theatre.
## Rate three dimensions
- **Value** — concrete user, product, operational, or maintenance benefit; include urgency.
- **Risk** — regression likelihood/impact: data loss, security, cloud incompatibility, disruption, hard rollback.
- **Complexity** — concepts, dependencies, layers, states, verification surfaces; not line count.
Use `Low`, `Medium`, or `High`. Give one evidence-based sentence each. No pseudo-precise scores.
```markdown
## Assessment
- Value: High — <concrete benefit>
- Risk: Medium — <failure modes and reversibility>
- Complexity: Low — <conceptual and verification burden>
- Recommendation: Proceed | Reshape | Defer — <why>
```
One assessment per overall change.
- High value never excuses unmanaged risk/complexity.
- High risk needs earlier proof, narrower increments, rollback, or stronger checks.
- High complexity needs clearer boundaries and smaller steps, not automatic abstraction.
- Low value plus high risk/complexity suggests reshape or defer.
- Revise after material scope discovery.
Plans: assessment before steps. Reviews: findings first, then assessment. Assessment informs; never replaces user intent or evidence.
+48
View File
@@ -0,0 +1,48 @@
# Simplicity and maintainability
Use for abstractions, comments, helpers, naming, or structural complexity.
## Simplicity
Choose the smallest explicit, testable design. No extension points, generic engines, wrappers, or config for hypothetical needs.
- Prefer direct flow over clever expressions or scattered conditions.
- Extract only to name a rule, enable pure tests, or remove meaningful duplication.
- Keep abstractions only when they reduce reader-held concepts.
- Reuse the concept owner when semantics match.
- Do not add flags, optional branches, generic names, or extension points to merge unlike cases.
- Prefer focused local helpers over generic APIs exposing unrelated modes.
- Keep scope narrow; note unrelated cleanup.
- Delete clearly obsolete branches, harnesses, and shims. Ask when ownership or compatibility is unclear.
## Comments
Keep comments for:
- non-obvious intent or invariants;
- necessary ordering, timing, mutation, or side effects;
- browser, Electron, cloud, or protocol constraints;
- workaround reasons and removal conditions;
- public contracts names/types cannot express.
Remove comments that:
- narrate code or names;
- number obvious steps;
- explain mechanics better expressed by code;
- preserve removed history;
- claim untested behaviour;
- use banners to hide oversized modules.
Update adjacent comments with code. Stale comments are defects.
## Naming and types
- Prefer Ontime terms over vague `data`, `result`, `item`.
- Prefer explicit/discriminated types over `any`, broad casts, optional fields, non-null assertions, silent fallbacks.
- Handle unions/enums exhaustively when future cases could be unsafe.
- Keep public functions focused enough to avoid long contract explanations.
## Review standard
Do not demand personal style. Report complexity only when it risks maintenance, hides rules, blocks focused tests, duplicates ownership, or makes changes unsafe. Suggest reuse only after verifying the existing contract fits without added genericity.
+46
View File
@@ -0,0 +1,46 @@
# Ontime domain invariants
Load only for touched domains. Add only stable, recurring invariants; not one-off bugs.
## Rundowns and entries
- Keep `entries`, `order`, `flatOrder` normalised.
- Keep group membership, group entry lists, child `parent` references consistent.
- Preserve entry identity and supported types across patch, clone, group, ungroup, reorder.
- Distinguish loaded vs background rundown. Prefer explicit rundown ID over global current state.
- No caller-owned rundown mutation unless explicitly contracted.
## Persistence, realtime, cache
- No partial commit on failure.
- Preserve revision/transaction semantics for loaded and background rundowns.
- Persist before websocket refetches, runtime updates, integration notifications, or cache assumptions.
- Notify only invalidated consumers; never leave client cache stale.
- Avoid duplicate listeners, notifications, invalidations, lifecycle effects.
- Reconnect/refetch must converge on authoritative state.
- Align query keys and websocket refetch keys with the changed resource.
## Timers
Use temporal values by meaning: `Instant` for epoch time, `TimeOfDay` for local time since midnight, `Duration` for elapsed time, `Day` for calendar offsets. Convert through `timeCore`; never interchange as raw numbers.
Active work: [runtimeState time-core migration](../migrations/runtime-state-time-core.md).
When relevant, cover interactions among:
- midnight/day offsets;
- linked events/gaps;
- delays/skipped entries;
- count-to-end;
- absolute/relative offsets;
- warning, danger, finish, roll, end-action transitions;
- loaded/next-event state.
Pass time/state explicitly to keep rules deterministic and unit-testable.
## Imports and migrations
- Treat project files, spreadsheets, custom fields, migrated data as untrusted.
- Preserve fields the import/migration does not own.
- Validate/parse into the current model before runtime logic.
- Avoid source mutation; test round trips and non-mutation when preservation matters.
+49
View File
@@ -0,0 +1,49 @@
# Routing and Ontime Cloud
Use for navigation, URLs, endpoints, websockets, auth, cookies, assets, redirects, presets, or local-storage scope.
## Deployment invariant
Support root and runtime-prefixed deployments:
```text
local: http://localhost:4001/timer
cloud: https://cloud.example/client-hash/timer
```
Prefix is deployment data. Never assume `/`.
## Client
- `apps/client/src/externals.ts`: derives `baseURI`, `serverURL`, `websocketUrl` from document base/current origin.
- `BrowserRouter`: uses `baseURI` basename.
- APIs/assets: use `common/api/constants.ts` or base-aware helpers.
- App navigation: use React Router. Never strip/guess/re-add prefix from `window.location.pathname`.
- Persisted browser state: use existing base-aware scoping where prefixes need isolation.
## Server
- `updateRouterPrefix()` in `apps/server/src/externals.ts`: normalises `ROUTER_PREFIX`.
- `apps/server/src/app.ts`: mounts routes below that prefix.
- Domain routers: paths relative to mount; never derive prefix.
- Websockets, auth redirects, cookie paths, share URLs: preserve prefix.
## URL rules
Use `URL`, React Router, or existing helpers instead of string manipulation. Preserve:
- leading/trailing slashes and runtime prefix;
- query params, auth tokens, navigation locks;
- preset aliases and canonical view paths;
- `https`/`wss` behind proxies;
- static/user asset paths.
Never infer Ontime Cloud from hostname alone. Generated base markup marks cloud; runtime prefixes also serve non-cloud reverse proxies.
## Cloud capabilities
Gate unavailable local-network integrations in cloud, including OSC output. Keep server behaviour and UI availability aligned.
## Verification
Test both root and a prefix such as `/client-hash`. Include relevant queries, redirects, cookies, websocket paths, presets, assets. Root-only routing coverage is incomplete.
+42
View File
@@ -0,0 +1,42 @@
# Security boundaries
Use for auth, external input, files, integrations, assets, URLs, or secrets.
## Untrusted inputs
Validate/parse before trust:
- HTTP bodies, params, headers, cookies, websocket messages;
- project files, migrations, spreadsheets, imports;
- custom HTML/CSS/views, translations, served assets;
- automation payloads, third-party responses;
- externally supplied paths/filenames.
Validate shape and domain constraints at the owning boundary. Keep existing parser/validation layers; avoid downstream defensive casts.
## Authentication and authorisation
- Keep protected routers behind auth middleware in `apps/server/src/app.ts`.
- Preserve auth across prefixed routes, redirects, websockets, generated links.
- Scope session cookies to runtime prefix; isolate hosted clients.
- Authentication never grants arbitrary file, path, project, or rundown access.
## Files, URLs, integrations
- Use established path/file helpers. Reject traversal and unexpected types.
- Build URLs with `URL` or existing helpers. Check open redirects, SSRF, protocol changes, token leaks.
- Encode/constrain untrusted HTML, CSS, filenames, headers, log values.
- Preserve cloud limits on local-network capabilities.
## Secrets and diagnostics
- Never commit/log passwords, hashes, tokens, credentials, cookies, private project content.
- Errors: useful, but no internal paths, stacks, credentials, sensitive payloads.
- Keep tokens in existing session/authenticated-share flows. Avoid new URL-token patterns.
## Review prompts
- Where does input become trusted?
- Validation once at owner, then useful type?
- Can one prefixed client cross another client's session/assets?
- Can logs, responses, redirects, URLs leak secrets?
+52
View File
@@ -0,0 +1,52 @@
# Testing strategy
Use for changed behaviour or tests.
## Layers
### Pure unit
Put detailed business-rule coverage on pure functions. Test public inputs/outputs with Vitest: relevant boundaries, invalid input, ordering, rollover, errors.
Every bug fix needs a regression test that fails before the fix. Prefer behaviour over mocks/implementation assertions.
### Service and state
Use service, DAO, store, or hook tests for orchestration: transitions, transactions, persistence, cache, notifications, external effects.
Do not repeat all pure cases here. Prove delegation and sequencing.
### End-to-end
Reserve Playwright for key journeys and high-risk cross-layer integrations: edit/run rundown, playback, imports, rundown switching, cloud-prefixed navigation.
No E2E for edges already proven in lower layers. Add E2E only when lower layers cannot prove the user-facing integration.
## Compact before handoff
Development harnesses may be broad, repetitive, diagnostic, temporary. Before handoff:
1. Identify distinct required behaviours/regressions.
2. Keep the smallest readable set that catches them.
3. Parameterise repetition only when the table reads better.
4. Remove diagnostic assertions, redundant permutations, temporary fixtures, private-detail coupling.
5. Keep rare cases that encode real domain rules.
Optimise for future readers, not minimum line count.
## Quality
- Name tests by observable behaviour.
- Keep setup local/explicit unless a fixture improves comprehension.
- Avoid arbitrary waits, wall-clock dependence, cross-test state, weak assertions.
- Prefer realistic typed fixtures over large snapshots or masking casts.
- Test non-mutation when promised.
- No tests for trivial type/format changes or framework behaviour Ontime does not own.
## Review prompts
- Business logic directly unit-testable?
- Test catches the reported bug/regression?
- Cases distinct, not repeated path?
- Temporary harness leaked?
- E2E justified by cross-layer risk?
+69
View File
@@ -0,0 +1,69 @@
# Workflow and repository conventions
Use for commands, imports, formatting, CI, dependencies, PRs.
## Workspaces
Confirm names from `package.json`.
| Package | Workspace name |
| ---------------- | --------------------- |
| Client | `ontime-ui` |
| Server | `ontime-server` |
| Electron | `ontime-electron` |
| Resolver | `@getontime/resolver` |
| CLI | `@getontime/cli` |
| Shared types | `ontime-types` |
| Shared utilities | `ontime-utils` |
Run from repo root: `pnpm --filter <workspace-name> <script>`. Add/install workspaces only when required.
## Verification
Start narrow; expand with risk:
```text
focused test
-> affected package tests
-> package lint/typecheck
-> required repository checks
```
```bash
pnpm --filter ontime-ui test:pipeline <path-to-test>
pnpm --filter ontime-server test:pipeline <path-to-test>
pnpm --filter ontime-utils test:pipeline <path-to-test>
pnpm --filter <workspace-name> lint
pnpm --filter <workspace-name> typecheck
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm e2e
```
PR CI: `.github/workflows/test.yml`; runs format, lint, types, unit, Playwright. Run E2E locally only for key flows or E2E infrastructure.
Before code commit: `pnpm lint`, `pnpm test`; add `pnpm typecheck` for TypeScript and `pnpm format:check` for formatted files. Claim only observed results.
## TypeScript and imports
- Strict TypeScript. Prefer `ontime-types` domain types over local copies.
- Keep client/server payloads aligned through `ontime-types`.
- Import `ontime-types`/`ontime-utils` from public entry points; Oxlint forbids `src` subpaths.
- Server relative ESM imports need `.js`.
- Reuse public exports/canonical helpers before adding utilities.
## Formatting and dependencies
- Use Oxlint/Oxfmt, not ESLint/Prettier.
- Oxfmt: two spaces, semicolons, single quotes, trailing commas, 120 columns.
- Add dependencies only when platform/current stack cannot solve the need. Review `package.json` and `pnpm-lock.yaml` together.
- Never hand-edit lockfile. No build output unless already tracked.
## Pull requests
- Title: `[<workspace-name>] <Title>`.
- Separate behaviour from unrelated refactors/format churn.
- State what changed, why, verification.
@@ -0,0 +1,67 @@
# runtimeState time-core migration
**Status:** In progress
## Goal
Make temporal meaning explicit across `runtimeState` and timer calculations. Prevent mixing:
- `Instant`: epoch timestamp;
- `TimeOfDay`: local milliseconds since midnight, range `[0, dayInMs)`;
- `Duration`: elapsed or remaining milliseconds;
- `Day`: calendar-day offset.
Use `apps/server/src/lib/time-core/timeCore.ts` for conversions and temporal arithmetic. Branded types remain numbers at serialization boundaries.
## Current state
Completed foundation:
- temporal brands in `ontime-types`;
- `timeCore` helpers for now, conversion, duration arithmetic, midnight crossing, calendar-day distance;
- partial `runtimeState` adoption of `Instant`, `TimeOfDay`, `Day`, and `timeCore`.
Remaining ambiguity:
- `TimerState`, `RundownState`, and `Offset` expose temporal fields as `number`/`MaybeNumber`;
- `timerUtils` accepts/returns raw numbers with different meanings;
- `runtimeState` retains raw duration fields, manual arithmetic, and `as TimeOfDay`/`as Duration` casts.
## Migration rules
- Classify each temporal field before changing it. No generic `Time` type.
- Convert only through `timeCore` or an explicit transport adapter.
- Keep public/websocket JSON numeric where required; brand at the boundary.
- Keep type migration separate from behaviour changes.
- Preserve current midnight, rollover, pause, add-time, roll, and offset behaviour per slice.
- Add helpers only when they encode a named temporal rule and remove caller casts/arithmetic.
## Slices
- [x] Add temporal brands and initial `timeCore` helpers/tests.
- [ ] Inventory temporal fields in `TimerState`, `RundownState`, `Offset`, and private runtime state; assign intended types.
- [ ] Migrate pure `timerUtils` functions by temporal concept; add focused midnight/rollover tests.
- [ ] Migrate private `RuntimeState` fields and calculations; remove local casts/manual conversions.
- [ ] Migrate shared runtime contracts and add numeric transport adapters where compatibility requires them.
- [ ] Update remaining callers, fixtures, and mocks.
- [ ] Remove superseded helpers, casts, and ambiguous temporal numbers.
Each slice must leave old/new boundaries explicit, compile cleanly, and preserve behaviour.
## Required coverage
- before, at, and after midnight;
- overnight and multi-day rundowns;
- local-day calculation across timezone/DST offset changes;
- pause/resume, added time, elapsed/remaining duration;
- roll secondary targets and expected finish;
- absolute/relative offsets and day offsets.
## Complete when
- Runtime/timer boundaries use semantic temporal types or documented numeric transport fields.
- Temporal conversions/arithmetic use `timeCore` or named pure helpers.
- No unexplained temporal casts or ambiguous numeric fields remain in migrated scope.
- Focused tests cover the required boundaries.
After completion, remove this migration tracker. Keep durable rules in `docs/agent-guides/domain-invariants.md`.