Commit graph

94 commits

Author SHA1 Message Date
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
Your Name
24558b1cae feat: pack annotation lanes into shared rows
The annotation lane above the tracks now collapses non-overlapping
annotations onto a single row. Each annotation is assigned a lane by
greedy first-fit in definition order (:created): it takes the topmost
lane it doesn't time-overlap, so overlapping annotations stack with the
earlier-defined one on top and the newer beneath.

- annotations carry :created (new ones stamp Date.now; seed has explicit
  values; old saved data defaults to 0 = oldest)
- pane order: by start, then longest-first so a section leads a tie
- seed data reworked to overlap (Act I / rally / baseline / glance / net,
  plus an overlapping nested child) so the packing is visible; one-time
  localStorage reset bumped so it lands

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:05:05 -04:00
Your Name
30cfbf3a1f feat: expandable annotations with nested timelines
Each annotation can be expanded into its own nested timeline. Reuses the
dormant :timeline-stack: annotations carry a :parent (:root = top level),
expanding pushes the annotation id, and only annotations whose :parent
matches the stack top are surfaced — so nested annotations stay invisible
on the main timeline.

- crop: the expanded annotation's own spans define a time window + the set
  of involved tracks; non-involved tracks collapse to height 0 (CSS-
  transitioned, so tracks animate away and survivors slide up), and the
  view offsets/crops to the window
- breadcrumb bar + back button (shown only when nested) fully restore the
  main timeline
- new annotations attach to the current context; the expand button shows a
  child count and delete removes the whole subtree
- seed data (Opening rally + 2 children, Net-cam beat) for testing, plus a
  one-time localStorage reset so it shows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:01:11 -04:00
Your Name
f67baf4b99 claude before limit 2026-06-27 13:07:42 -04:00
Your Name
6ed11fbca5 local storage, mobile improvements, re-frame refactor, etc 2026-06-27 12:56:13 -04:00
Your Name
38367cd052 vaarious fixes 2026-06-27 12:00:43 -04:00
Your Name
bdc9928db8 feat: init 2026-06-27 11:11:20 -04:00