Commit graph

56 commits

Author SHA1 Message Date
Your Name
a62af2584b Revert "fix: play from the playhead, not currentTime (iOS deep-link seek)"
This reverts commit 5c58af9036.
2026-06-30 11:17:42 -04:00
Your Name
484351746f Revert "fix: defer video seek until seekable, flush on loadedmetadata (iOS)"
This reverts commit 7585dd4e7f.
2026-06-30 11:17:42 -04:00
Your Name
7585dd4e7f fix: defer video seek until seekable, flush on loadedmetadata (iOS)
The previous fix still failed on iOS: at the first tap the element isn't
seekable (readyState 0), so the seek in engage-play! was dropped, and the
existing on-loadedmetadata seek is guarded by (when-not @play) — already true by
then — so it was skipped too, playing from 0. Now seek-video! stashes the target
when the element isn't seekable yet, and loadedmetadata/canplay flush it
(regardless of play state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:02:54 -04:00
Your Name
5c58af9036 fix: play from the playhead, not currentTime (iOS deep-link seek)
On iOS the element doesn't load (so a deep-linked pre-seek doesn't stick) until
the user's first tap, so engage-play! read currentTime=0 and played from the
top. Seek to the playhead inside the play gesture and drive from there; for
normal pause/resume the playhead already equals currentTime, so it's a no-op.
Drops now-unused seg-at-src.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:54:53 -04:00
Your Name
28aa2622f6 ui: unhighlight between marks; translucent clip label
- ::active-annotations no longer falls back to the nearest annotation, so when
  the playhead is outside every mark nothing is highlighted.
- clip label background is translucent (color-mix paper 70%) like the lane
  bars, so the thumbnail reads through while text stays legible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:47:33 -04:00
Your Name
d877655b7a ui: bolder/colored active annotation, readable clip labels, multi-active
- active annotation is now loud: thick colour spine, a wash of its own colour,
  a hard offset shadow, bold title (was just a faint 2px shadow). Annotation
  colour drives it via a --ann-color custom property; added a colour swatch by
  each title so the commentary reads its colours.
- clip name is a solid bordered label chip pinned top-left instead of black
  text + white halo, which muddied over busy thumbnails.
- highlight ALL annotations whose marks contain the playhead, not just one
  (new ::active-annotations set sub), so overlapping marks light up together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:45:34 -04:00
Your Name
7e32dc70dd feat: PWA restores last project + playhead/stack on reopen
A homescreen launch opens the manifest start_url ("/") with no route, so the
deep state was lost. Persist the in-app URL (already kept current with stack +
playhead) to localStorage, and on a standalone cold launch with a bare hash,
re-enter it before the router reads the URL. Browser tabs are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:45:34 -04:00
Your Name
b031a167a4 fix: clip-follow only when playhead is inside a scroll-to annotation's bar
::active-annotation falls back to the nearest annotation (it drives the
commentary highlight), so gating playback clip-follow on it fired for every
clip. Gate on strict bar containment of a :scroll-to annotation instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:37:49 -04:00
Your Name
baa46cb734 feat: jumps always scroll to clip; scroll-to flag = clip-follow on playback
Reframe the per-annotation :scroll-to flag. Jumps/links now ALWAYS scroll the
target clip into view (unconditional, no flag). The flag instead drives normal
playback: while the playhead plays through an annotation that opts in, each of
its clips is scrolled into view as the playhead crosses onto that clip's track
(deduped to fire only on track change; reset when playback stops). Form label
updated to "Follow clips during playback".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:33:21 -04:00
Your Name
a99485ff48 refactor: scroll-to-clip via scrollIntoView on the gutter label
Replace the manual lane-h/row-h/clientHeight math (fragile, clamped oddly for
top rows) with scrollIntoView on the target track's gutter label. The label
lives in the fixed track-name column, so the browser scrolls the vertical axis
only — clean, no geometry, and structurally can't fight the horizontal follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:28:00 -04:00
Your Name
602c710377 feat: per-annotation scroll-to flag gating vertical scroll on jump
Add :meta {:scroll-to bool} to annotations, toggled by a "Scroll to clips on
jump" checkbox in the annotation form. goto! now separates follow? (horizontal
recenter) from scroll-to? (vertical scroll to the clip's track); the vertical
scroll only fires for annotations that opt in, resolved from the canonical
scene group at each jump site (jump button/popover, script highlight, link
chips). Persists for free via editable-group + the backend's opaque group
merge; round-trips through json keywordize, so no migration needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:19:01 -04:00
Your Name
0c305e42ea feat: smooth horizontal + vertical scroll on jump/link
follow! gains an optional smooth flag. The jump/link path (goto! with follow?)
now glides both axes; playback-follow stays instant (smooth would fight the
per-frame updates) and scrubbing doesn't scroll at all, so clicking stays snappy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:10:55 -04:00
Your Name
967ca97e84 feat: scroll vertically to the clip's track when jumping via link/jump
goto! with follow? already recentred horizontally on the playhead. Now it also
scrolls the tracks container vertically so the clip under the landing frame is
brought into view — for link chips, annotation jump buttons, and script
highlights. No-op when the tracks already fit. Accounts for the pinned
annotation-lane header height in the scroll math.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:04:25 -04:00
Your Name
96da4a6bd8 feat: HyperCard-style top title bar (logo, project name, share, user menu)
Replace the mid-page .project-bar with a proper top nav bar pinned above the
video/timeline, styled like a classic Mac window title bar (centered title
flanked by racing stripes).

- left: TL wordmark + thumbnail-status indicator
- center: ‹ back-to-projects + project name, between the title stripes
- right: Share popover and a user dropdown with Log out
- Share offers two clearly named links, copied to clipboard:
  "Copy link to this moment" (current context + playhead) and
  "Copy link to the whole project" (root)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:58:54 -04:00
Your Name
28aafc047f fix: thumbnail parser overflow, autostop-killed jobs, unified playhead
Thumbnails failed immediately on any clip past ~2 min. The one-pass select
expression joined frames with a flat a+b+c+... chain, which overflows ffmpeg's
recursive expression parser past ~120 terms ("Error while parsing expression").
Join the same terms as a balanced tree (depth O(log N)) instead — identical
selection, still one decode pass, exact per-tile frames preserved.

- generate_thumbnails management command: runs synchronously and streams each
  phase to stdout, so jobs are watchable without tailing app logs
- surface ffmpeg's stderr instead of swallowing it in CalledProcessError
- fly: min_machines_running = 1. Background thumbnail jobs outlive the HTTP
  request that triggers them, so scale-to-zero let the proxy autostop the VM
  mid-job and leave projects stuck "running"
- timeline: replace the two split playhead bars (pinned lanes vs scrolling
  tracks) with one full-height overlay, positioned in JS to track horizontal
  scroll and sit above the translucent header

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:48:08 -04:00
Your Name
20ae2b4316 actually scroll to playhead 2026-06-30 02:52:38 -04:00
Your Name
96639554e4 translucent stuff etc 2026-06-30 02:42:31 -04:00
Your Name
5bc53c03f3 fix: generate thumbnails in one ffmpeg pass, not ~N/30
ffmpeg's select filter re-decodes the whole clip on every pass, so chunking by 30
turned a 670-frame job into 23 full decodes (minutes on shared CPU — long enough
to be killed mid-run and leave thumbnail_status stuck at 'running'). One pass now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:36:27 -04:00
Your Name
b05b440922 feat: upload a script PDF from the script pane
Backend: POST /api/projects/<id>/script/ (owner/collaborator) saves the script
FileField and returns the refreshed detail. Frontend: when a project has no
script, the script pane shows a PDF file picker that uploads on selection; the
returned detail carries the new URL so the pane renders it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:09:16 -04:00
Your Name
9c4b56dbc6 feat: per-annotation schema version + migration framework
Each stored annotation carries :v (schema-version, currently 1). tl.scene gains
a transformer registry and migrate fn that chains version upgrades on load;
save-group and open-draft stamp the current version. Empty transformer set at
v1 — bump schema-version and add a transformer when the model changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:06:14 -04:00
Your Name
7e47ec6a2d feat: upload progress on new-project form
Swap the create-project upload from :http-xhrio to a raw XHR effect with
upload.onprogress, surface it as a 1-bit progress bar; disable Create while
uploading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:03:42 -04:00
Your Name
533d14ce49 ui: monochrome unicode glyphs + zoom arrow icons
- Zoom X/Y sliders labelled ↔ / ↕
- dark toggle ◐/◑, pause ❚❚, warnings △, drop 🔗 — no color emoji in the
  1-bit theme

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:01:27 -04:00
Your Name
ed5db0ce17 rename app UI to tl (Fly app/domain stay lumet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:56:22 -04:00
Your Name
e6d150c375 feat: production deploy (Fly + R2) + frame controls and picker glow
Deploy:
- Multi-stage Dockerfile (shadow-cljs release → Python+ffmpeg runtime), fly.toml
  (one machine, scale-to-zero, SQLite on a volume), .dockerignore, DEPLOY.md
- Same-origin release build: api-port defaults "9001" for dev, "" in release
  (no CORS in prod)
- env-driven settings (SECRET_KEY/DEBUG/ALLOWED_HOSTS/DB_PATH/CSRF), WhiteNoise
  serving static + the SPA, R2 (S3-compatible) media storage when R2_* is set
- thumbnails reworked storage-agnostic: download clip, ffmpeg select-pass, upload
  tiles via default_storage (R2 in prod, disk in dev)
- pin channels/daphne; add whitenoise/django-storages/boto3

UI:
- frame step buttons flanking play (disabled at timeline bounds)
- Add-annotation button moved into the toolbar, made prominent
- picker mode pulses a 1-bit glow on clips / frame readout / (while linking)
  annotation bars; linking an annotation bar inserts a timeline link

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:56:09 -04:00
Your Name
e00a056209 feat: HyperCard theme + dark mode
- Retheme the whole app 1-bit HyperCard: white cards, hard 1px black borders,
  Chicago/Geneva/Monaco fonts (with web fallbacks; real fonts go in /fonts, see
  fonts/README.md), inverted-on-press buttons, a 50% desktop dither, black Mac
  title bar. Palette runs on CSS variables.
- Dark mode toggle (toolbar + home header): flips the palette vars via a class
  on <html>, persisted in localStorage and applied before first paint.
- Per-annotation fat colored left stripe in the annotation pane; annotation name
  bumped above the md h1; full-color (no longer veiled/desaturated) thumbnails.
- Jump popover now labels each part with the clip name + crop-adjusted mark-time
  frame (e.g. "CU Tashi (1) 45f"), matching the annotation editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:23:34 -04:00
Your Name
fc25dfc016 feat: timeline links, network error handling, Tailscale access, mobile fixes
Backend / dev:
- Bind dev server to 0.0.0.0 and allow Tailscale MagicDNS + tailnet-IP origins
  in ALLOWED_HOSTS / CORS / CSRF so the app is reachable over Tailscale
- Unified, idempotent dev launcher (Django + media + shadow watch), hot reload,
  no manual compiles

Frontend:
- Migrate fetches to re-frame :http-xhrio with failure handling; surface
  network / load / save errors where the user expects them
- Extract markdown + link formatting into tl.md: parse/serialize round-trip
  (recursive chip serialization fix), block markdown (headings/lists/paragraphs)
- Timeline-opening links: navigate the stack to any reachable timeline;
  autocomplete lists all timelines (root included, orphans dropped) with a live
  preview that pushes the stack and reverts on commit/cancel
- Annotation pane shows the current context's description with edit-in-context
  (drops into the parent timeline for marks, pops back when done) + root content
- Fixes: deep-link playhead seek, replay-from-end, mobile input zoom, notch
  safe-area insets, autofocus the content field, freeze form to snapshot context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:24:40 -04:00
Your Name
3d3b031332 feat: add route state and merge patches 2026-06-29 14:24:49 -04:00
Your Name
5b206635fb feat: reuse marks picker for annotation links 2026-06-29 12:14:08 -04:00
Your Name
d3c8dcbdf2 Script pane, realtime peer sync, and logout button
- Script pane: PDF tab beside annotations; "Add script annotation" arms
  highlight mode on the draft, ✓/✕ to confirm/cancel, auto-scrolls to the
  active annotation's highlights during playback. Adds Project.script.
- Realtime: Django Channels websocket per project broadcasts attributed
  scene deltas after each save; peers merge them in (keeping local drafts).
- Logout link in the editor project-bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:52:46 -04:00
Your Name
595ac5ba72 json bug fix 2026-06-29 04:01:52 -04:00
Your Name
1b0dd21fb1 Show thumbnails in expanded timelines 2026-06-29 03:36:07 -04:00
Your Name
caa2aa6b9b thumbnails 2026-06-29 03:19:47 -04:00
Your Name
5861113aeb Drive playhead from the video element; reseek on stack moves
- the <video>'s own play/pause now engage/disengage the segment-aware tick
  (:on-play/:on-pause), so the native controls drive the playhead and follow the
  multicam cuts, not just the toolbar button (which now just toggles the video)
- entering/leaving a context (expand/collapse/pop-to) pauses and seeks the video
  to that context's remembered playhead ([:view :playheads ctx], 0 = its start),
  so the playhead tracks the timeline stack instead of ticking a stale context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:42:40 -04:00
Your Name
8ee2cdf7b9 Lay root clips at timeline positions, not source positions
The root timeline placed each clip at its media-in (source) frame, which is
fractional (media-offset 50641.358…), so adjacent clips of a contiguous multicam
edit showed ~1-frame gaps; clicking a gap hit no segment and local->source
returned the last frame ("glitch to the end"). The OTIO timeline is gapless —
clips tile by their accumulated timeline :start. Carry :start through from-otio
and lay content-segments at [start, start+len] (src stays the source range), so
the assembled program is contiguous and maps cleanly to scattered source frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:34:11 -04:00
Your Name
0d73b204e6 Coalesce continuous annotation bars into one jump point
A continuous selection across N clips is N marks → N bars, and OTIO's fractional
media offsets leave ~1-frame gaps between adjacent clips, so the jump control
showed "jump (5)" for a continuous stretch. merge-bars snaps bar endpoints to
whole frames and joins touching ranges; only a real >=1-frame gap splits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:25:59 -04:00
Your Name
fcac22fe83 Give new annotations unique ids (gensym reset clobbered old ones)
open-draft used (gensym "ann-"), whose counter resets on every page load, so the
first annotation added after a refresh reused a prior gid (ann-2, …) and the
per-id merge overwrote the existing annotation. Use a uuid-based gid instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:20:24 -04:00
Your Name
ceccec8060 Fix annotations not persisting across reload (keyword round-trip)
Keyword *values* don't survive JSON: clj->js stringifies :annotation/:root/clip
refs and js->clj only re-keywordizes keys, so a reloaded annotation had string
:type/:parent/:ref and was filtered out of the scene. Re-keywordize those fields
on load (scene/restore-annotations) and give marks string ids (random-uuid was
serializing as a UUID object).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:10:15 -04:00
Your Name
0a842c8852 Point frontend at backend on port 9001
8000/9000 were occupied by other local services; use 9001.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:01:06 -04:00
Your Name
20cd3cc6e3 Wire cljs frontend to the backend with session login
- frontend: hash routing (project list / create / editor), per-project init
  (load OTIO + scene + clip from the API), create form with otio + clip upload,
  annotation saves/deletes pushed as deltas, edit/add buttons gated on auth
- auth: own login form in cljs posting to session login/logout endpoints
  (httpOnly cookie — no JWT in JS); /api/me/ drives the UI's signed-in state
- backend: add /api/login/ and /api/logout/ (session auth)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 01:54:22 -04:00
Your Name
94475da936 Project create/list API, clip uploads, CORS, public read
- Project gains a clip FileField; POST /api/projects/ creates one from an
  uploaded otio + clip (<=100 MB), owned by the requester
- add /api/me/ and project-detail; scene GET is public (view without login),
  PUT stays owner/collaborator-only
- serve media in DEBUG (range requests for video seek); CORS for the cljs dev
  origin with credentials
- seed_demo now builds the Challengers project from the bundled otio + 480p clip

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 01:39:54 -04:00
Your Name
60fb4df795 Merge scene saves as per-id deltas instead of clobbering
PUT now takes {changed, deleted} and merges per annotation id, so concurrent
edits to different annotations both survive; same-annotation edits are
last-write-wins by arrival. No version/locking. No-op saves don't write a
revision. Tests cover merge, LWW, stale-client safety, attribution
(server-stamped/spoof-resistant, creator preserved), revisions, and access.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 01:34:52 -04:00
Your Name
36054ac329 Attribute annotations and scene saves to users
Server-authoritative attribution: on each scene PUT the server diffs incoming
annotation groups and stamps createdBy/editedBy (+ timestamps) from request.user,
ignoring client-sent stamps so authorship can't be forged. Each save also writes
a Revision (user, time, +N ~N −N summary, snapshot of the annotation layer),
visible in the admin and via /revisions/. Projects gain collaborators so several
users can edit one project and get distinct attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 00:12:13 -04:00
Your Name
57ef570539 Add Django backend: users, admin, OTIO + scene persistence
A Project (owner, OTIO file, fps, scene JSON) persists the timeline. Session
API: list projects, GET/PUT scene, serve OTIO. Admin manages users/projects;
seed_demo creates a project from the bundled one_two_three.otio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:45:49 -04:00
Your Name
7a960afb5c Detect broken child mark-refs; restore the timeline/top divider
- scene: point-frame rejects out-of-range offsets so broken-marks catches
  boundary shifts and cascades, not just deleted refs; add broken-reason
- subs: annotations carry :broken/:reason and sort broken ones to the bottom
- views: ⚠ on broken annotations (hover = reason); drag-resize the top/timeline
  split via --top-h (vh)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:45:43 -04:00
Your Name
c03756d3e1 Rebuild annotation editor on the mark-group model
- scene: mark-group model (resolve/selection->marks/seg-local/marks->rows);
  drop legacy tl.marks/tl.md
- authoring: draft is a normal annotation group flagged :draft; generic
  put-group + with-let cancel; two-input click-to-fill editor in mark time
- timeline: dashed draft preview, numeric track ordering
- video frame readout (track+clip, mark time) clickable to fill an endpoint
- discontinuity jump popover; nested-annotation count badge
- localStorage persistence of the authored annotation layer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:18:07 -04:00
Your Name
9d0302c367 data model added 2026-06-28 14:02:14 -04:00
Your Name
ea181911cb clearing everything starting again basically 2026-06-28 14:02:11 -04:00
Your Name
60d1a641ee feat: broken fixes lol 2026-06-28 09:12:28 -04:00
Your Name
ec236e93cb feat: prev/next frame buttons flanking play
Transport row is now |◀ ▶/⏸ ▶|. step-frame! pauses, rounds the video
clock to the nearest frame and seeks ±1; the rAF clock reports the new
position back into :playhead, so the red line follows (same single
seek-only path as jump/scrub).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:19:02 -04:00
Your Name
ea78a9ee08 fix: video won't seek — playhead clock had no element handle
seek-to-frame! and the rAF clock both reach the <video> through the
shared video-el atom, which was populated by an inline :ref closure —
recreated every render and callable with nil by React, so video-el ended
up nil. That made set! currentTime (jumps + scrubs) and the clock's
currentTime read both silent no-ops: the red line moved (separate
:playhead dispatch) but the picture never seeked or followed.

Capture the element with a stable ref that ignores nil, and collapse the
clock to a single source of truth: one always-on rAF copies currentTime
into :playhead, and jump/scrub only seek (no optimistic :playhead write),
so the line and the picture can't disagree. Replaces the old
push!/tick/stop + on-play/on-pause/on-seeked bookkeeping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:05:14 -04:00