docs(spec): format finder spec

The unit-test job runs format:check before the tests, so an unformatted
markdown file failed the job while every test passed.

Promote the bold pseudo-headings in the Phase 3 section to real
subheadings. They previously sat directly above numbered list items,
which the formatter reads as one paragraph and collapses into a run-on
line, mangling items 13-27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzALEq9gGWwFmwTdgAiFcY
This commit is contained in:
Claude
2026-08-18 11:44:21 +00:00
parent cabfc592df
commit f34ce5bc3d
+31 -26
View File
@@ -11,12 +11,12 @@ business-development one: is it worth investing in, and can it beat the competit
Scope decided for the roadmap: **editor + cuesheet**, **navigation-only** (the Finder reveals
entries, it does not act on them), and **results stay in rundown order** — no ranking.
| File | Role |
|---|---|
| `apps/client/src/views/editor/finder/Finder.tsx` | Modal, input, result list, footer hints |
| `apps/client/src/views/editor/finder/useFinder.tsx` | Search logic |
| `apps/client/src/views/editor/finder/Finder.module.scss` | Styling |
| `apps/client/src/features/rundown/placements/FinderPlacement.tsx` | `mod+F` hotkey + mount |
| File | Role |
| ----------------------------------------------------------------- | --------------------------------------- |
| `apps/client/src/views/editor/finder/Finder.tsx` | Modal, input, result list, footer hints |
| `apps/client/src/views/editor/finder/useFinder.tsx` | Search logic |
| `apps/client/src/views/editor/finder/Finder.module.scss` | Styling |
| `apps/client/src/features/rundown/placements/FinderPlacement.tsx` | `mod+F` hotkey + mount |
---
@@ -61,7 +61,7 @@ is hardest.
### 1.4 Reach
Editor-only. But `apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx:160` already
registers a scroll handler commented *"for explicit jumps (finder/keyboard)"* — the plumbing was
registers a scroll handler commented _"for explicit jumps (finder/keyboard)"_ — the plumbing was
anticipated and built, the Finder was never mounted there. Cheapest high-value win available.
**Constraint:** the cuesheet is `permission='operator'` and can be exposed via URL presets with
@@ -92,7 +92,7 @@ results, so every behaviour described above is unguarded.
**A runtime-aware, multi-surface finder is a real wedge.** [Shoflo](https://shoflo.tv/),
[Rundown Studio](https://rundownstudio.app/) and [Cuez](https://cuez.app/script-rundown/) are cloud
rundown *editors* — their search is document search over a document you're authoring. Ontime knows
rundown _editors_ — their search is document search over a document you're authoring. Ontime knows
show state: what's loaded, what's past, the offset, what's flagged. A finder that marks the loaded
event and shows scheduled time and delay per row is a different category of tool, and that data
already exists in `RundownMetadata` and is currently discarded. Add that it works identically on the
@@ -113,7 +113,7 @@ Sequence accordingly.
Estimates assume one developer familiar with the codebase.
### Phase 1 — Correctness *(~0.5 day)*
### Phase 1 — Correctness _(~0.5 day)_
1. **Track `selectedId`, not `selectedIndex`** — derive the active row by id lookup, falling back to
the first. Correct across updates, reordering and result changes; better than "reset to 0", which
@@ -129,7 +129,7 @@ Estimates assume one developer familiar with the codebase.
latent conflict with inline field editing.
6. **Fix the `index <n>` bound** to use the event count.
### Phase 2 — Search depth *(~1.52 days)*
### Phase 2 — Search depth _(~1.52 days)_
7. **Search cue + title + note + text custom fields** across events, groups and milestones on a bare
query. Skip `image`-type custom fields. Cap indexed note length so one pasted script can't
@@ -138,8 +138,8 @@ Estimates assume one developer familiar with the codebase.
for every key so today's documented syntax keeps working. Drive the footer hint and the
`EventEditorEmpty.tsx` cheat sheet from one filter-key constant so they can't drift.
9. **Keep rundown order — no ranking.** Deterministic, simpler, and it preserves the early-exit scan
(today's cap of 12 is a *scan* cap, not a sort cap; ranking would force scanning everything).
*One honest consequence:* widening the searched fields while keeping position order means a
(today's cap of 12 is a _scan_ cap, not a sort cap; ranking would force scanning everything).
_One honest consequence:_ widening the searched fields while keeping position order means a
distant cue match can be pushed off the list by nearer note matches. Two mitigations, both cheap
— raise the cap and show "showing 12 of 47", and label which field matched so a note hit is
obviously a note hit. If that still isn't enough in practice, group by matched field (cue block,
@@ -152,18 +152,20 @@ Estimates assume one developer familiar with the codebase.
Highest-value case: `index <n>` staying aligned with 1-based UI event indices when delays, groups
and milestones interleave — the current code gets this right and a rewrite is likely to break it.
### Phase 3 — Visibility, UX and polish *(~1.5 days)*
### Phase 3 — Visibility, UX and polish _(~1.5 days)_
This is the phase that changes how many people ever use the feature.
**Visibility**
#### Visibility
13. **Add a search control to `RundownHeader.tsx`** with the `mod+F` hint visible on it. Single
biggest discoverability win.
14. **Add one to `RundownHeaderMobile.tsx`** — mandatory, not optional: without a keyboard the
feature currently does not exist on touch devices.
15. **Add one to the cuesheet toolbar** when the Finder mounts there (Phase 4).
**UX**
#### UX
16. **Fix the empty state** — it shows "No results" before you've typed anything. Show the filter
hints, or recent searches, on open.
17. **Align with the app's other search box.** `SettingsSearch.tsx` has a leading `IoSearch` icon and
@@ -175,7 +177,8 @@ This is the phase that changes how many people ever use the feature.
21. **Preserve the last query on reopen**, selected so typing replaces it.
22. **`Home` / `End` / `PageUp` / `PageDown`** in the result list, matching `useRundownKeyboard`.
**Polish**
#### Polish
23. **Use `getAccessibleColour`** (`common/utils/styleUtils`) for the index badge. It currently sets
`background: var(--color)` raw with fixed foreground text, so a light entry colour is unreadable.
The cuesheet `EventRow`, `MilestoneRow` and `OperatorEvent` already do this correctly — the
@@ -187,7 +190,7 @@ This is the phase that changes how many people ever use the feature.
27. **Reconsider the 100 ms debounce** — with a controlled input (item 11) it's unnecessary at these
rundown sizes and just reads as lag.
### Phase 4 — Cuesheet *(~1 day)*
### Phase 4 — Cuesheet _(~1 day)_
28. **Move the Finder out of `features/rundown/placements/`** — it stops being an editor view once it
has two homes — and mount it in the cuesheet. Pass surface and permission explicitly from the
@@ -198,10 +201,10 @@ This is the phase that changes how many people ever use the feature.
delay per row. Source at render time, not by rebuilding the index (§1.5). This is the
differentiator from §2.
31. **E2E spec** alongside `209-rundown-shortcuts.spec.ts`, covering both surfaces, with a regression
lock for the click bug: click the third result *without* hovering the first two, assert the third
lock for the click bug: click the third result _without_ hovering the first two, assert the third
entry is selected.
### Phase 5 — Find and replace *(~34 days)*
### Phase 5 — Find and replace _(~34 days)_
The strongest expansion, and a better business case than a command palette: renaming a sponsor,
speaker or venue across a 200-entry rundown is a real recurring pain that the competitors'
@@ -221,6 +224,7 @@ and replace all agree on what "matches" means.
**32. The server gap — this is the actual work.** `batchEditEntries` (`rundown.service.ts:166`)
applies **one patch to many ids**. Find-and-replace needs **per-entry distinct values** — each
entry's own title with its own substring swapped. Two options:
- N × `putEditEntry`: N round trips, N revision bumps, N `notifyChanges` broadcasts. During a show
that's N timer notifications for one user action. Not acceptable at 40 entries.
- **A new endpoint taking `Array<PatchWithId<OntimeEntry>>` applied inside one
@@ -229,18 +233,19 @@ entry's own title with its own substring swapped. Two options:
possible.
**33. Replaceable fields — deliberately narrow.**
- *Safe:* `title`, `note`, text `custom` values. Free text, no structural meaning.
- *Opt-in and validated:* `cue`. It's a numbering scheme, not free text — `cueUtils` has
- _Safe:_ `title`, `note`, text `custom` values. Free text, no structural meaning.
- _Opt-in and validated:_ `cue`. It's a numbering scheme, not free text — `cueUtils` has
`getIncrement`/`getCueCandidate` and the Renumber dialog exists precisely because of that. The
server also rejects an empty cue outright (`rundown.service.ts:177`), so a replace that empties one
fails the whole batch.
- *Never:* times, booleans, colours, enums, ids, `image`-type custom fields.
- _Never:_ times, booleans, colours, enums, ids, `image`-type custom fields.
**34. Preview is mandatory, because there is no undo anywhere in this app.** I checked — the client
has no undo stack of any kind. Find-and-replace would be the first feature that can silently alter
dozens of entries. So: show every affected entry with before → after per field, with a per-match
opt-out, before anything is written. Confirmation names the count and the field: *"Replace 'Q' with
'CUE' in title on 23 entries?"*
opt-out, before anything is written. Confirmation names the count and the field: _"Replace 'Q' with
'CUE' in title on 23 entries?"_
**35. A single-step revert.** Because the atomic endpoint gives one revision per operation, capturing
the prior values client-side makes "Undo replace" a second batch call. Not an undo history — one
@@ -256,7 +261,7 @@ moved. Prevents replacing text the user never saw someone else write. Cheap.
**38. Editor only for v1.** The cuesheet is `permission='operator'` and its URL presets can be
read-only or column-restricted; a correct implementation would have to gate every field through
`getCuesheetColumnAccessPolicy().canWrite(key)`. *Finding* is safe to expose broadly; *replacing* is
`getCuesheetColumnAccessPolicy().canWrite(key)`. _Finding_ is safe to expose broadly; _replacing_ is
not. Warn when playback is running rather than blocking.
**39. No regex.** Live operators, no undo history, and one bad pattern destroys a rundown.
@@ -276,7 +281,7 @@ tests ~1d.
- **Virtualising the result list.** A palette showing 400 rows isn't more useful than one showing 50;
scanning is the bottleneck, not rendering. Cap and count instead.
- **Implicit cross-rundown search.** Revealing an entry in another rundown means changing the
*loaded* rundown — a destructive runtime action mid-show. Only the cuesheet has a non-destructive
_loaded_ rundown — a destructive runtime action mid-show. Only the cuesheet has a non-destructive
"viewed rundown" concept. If ever wanted, an opt-in `rundown:` filter there, never in the editor.
- **The operator view and public viewers.** The operator view never calls `setScrollHandler`, so
"go to entry" would silently do nothing until one is registered.