Vellum/web
mithral 785d3ed5ee Waves 4-6: audio depth, ffprobe facts, and direct-play video
Direct play only. The server is a headless GPU-less Xeon: nothing is ever
transcoded or remuxed. A file either plays in the client or it does not.

Measured, not assumed (ffmpeg-generated files with known codecs, played through
Vellum's own stream endpoint in Chrome 155 / Linux):
  mp4  h264+aac   plays        mkv h264+aac  PLAYS   <- container is fine
  mov  h264+aac   plays        webm vp9/opus plays
  mp4  av1+opus   plays        webm vp8/vorbis plays
  mp4  hevc+aac   AUDIO ONLY, videoWidth stays 0, no error
  mkv  hevc+flac  AUDIO ONLY
  avi  mpeg4+mp3  fails outright
All seven audio formats play. Two conclusions drive the design: codec matters
and container barely does (mkv+h264 plays, mp4+HEVC does not), and canPlayType
is unreliable in both directions — it says "no" for mov+h264 (which plays) and
"maybe" for mkv+hevc (whose video never decodes).

Therefore playability is a property of the CLIENT, not the file. The server
never declares a file playable; it reports codec facts and each client decides.
That is what makes adding mpv/libvlc/MDK later a matter of registering an engine
rather than re-architecting.

Wave 4 - audio depth
- New MusicPlayer.svelte: real queue, shuffle that walks a fixed permutation
  (so "previous" means something and nothing repeats early), repeat off/all/one,
  per-track rows, album art, next-track preload.
- AudiobookPlayer: MediaSession (OS media keys + lock screen), playback speed
  that SURVIVES a chapter change and persists, sleep timer with countdown that a
  manual pause cancels, keyboard shortcuts, next-chapter preload.
- Shared mediaSession.ts written by the coordinator before dispatch so both
  players consume one implementation instead of inventing two.

Wave 5 - ffprobe facts
- internal/probe (ffprobe wrapper, optional dependency), migration 0010 adding
  vcodec/acodec/width/height/probed_at to media_files, scanner hook, API fields.
- Probing runs only for NEW or CHANGED files and BEFORE the write transaction
  opens, so it can never hold SQLite's write lock and a rescan never becomes a
  full-library ffprobe sweep. Proven: 797ms probing scan, then 1ms rescan with
  probed_at byte-identical.
- ffprobe skips attached_pic so an mp3 with cover art reports no video codec.
- Fixed a real hole: .avi is in the video registry but had no MIME entry, so it
  was served as application/octet-stream and could never play.

Wave 6 - the video player
- VideoPlayer.svelte with resume, progress + keepalive final flush, MediaSession,
  keyboard shortcuts, multi-file picker, and codec facts shown.
- The honest path is the feature: an HEVC file makes Chrome play audio over a
  blank frame with no error, which is the worst possible outcome. The player
  detects it and says so, naming the codec and pointing at clients that can.

Coordinator fix to Wave 6: the agent's detection hung off `loadedmetadata`, but
an explicit `type` the browser cannot satisfy makes it reject the source during
selection, so NEITHER loadedmetadata NOR error fires — and the player showed a
working-looking 0:00 with a Play button, exactly the silently-broken state the
component exists to prevent. Added a load watchdog: if the element says nothing
within 2.5s, the source was rejected and the honest notice is shown. Verified
per file afterwards (h264/mkv play at 640 wide; hevc and mpeg4 show their
notices).

Verified end to end, not just compiled: 15 packages pass (including the new
internal/probe), gofmt/vet clean, svelte-check 0/0, and every claim above checked
against a running server. Note the demo's own mp3s are 100 KB of random bytes and
cannot decode; the audio players were proven against generated real audio instead.
2026-10-11 14:14:32 -04:00
..
public Apply the brand: charcoal, blue-violet, purple, and the real logo 2026-10-10 17:41:49 -04:00
src Waves 4-6: audio depth, ffprobe facts, and direct-play video 2026-10-11 14:14:32 -04:00
.gitignore Web UI: Svelte app, embedded into the binary 2026-10-09 16:40:58 -04:00
embed.go Web UI: Svelte app, embedded into the binary 2026-10-09 16:40:58 -04:00
index.html Make the brand the only theme 2026-10-11 13:41:55 -04:00
package.json web: type process.env in vite.config (add @types/node) 2026-10-10 16:33:05 -04:00
pnpm-lock.yaml web: type process.env in vite.config (add @types/node) 2026-10-10 16:33:05 -04:00
pnpm-workspace.yaml Web UI: Svelte app, embedded into the binary 2026-10-09 16:40:58 -04:00
README.md Web UI: Svelte app, embedded into the binary 2026-10-09 16:40:58 -04:00
svelte.config.js Web UI: Svelte app, embedded into the binary 2026-10-09 16:40:58 -04:00
tsconfig.json web: type process.env in vite.config (add @types/node) 2026-10-10 16:33:05 -04:00
vite.config.ts dev: fix the Vite proxy and add a one-command dev runner 2026-10-10 16:32:32 -04:00

Vellum web

Svelte 5 + Vite + TypeScript single-page app. It is built to a static bundle in dist/ and embedded into the Go binary, so all asset paths are relative (base: './' in vite.config.ts).

Commands

pnpm install
pnpm dev      # dev server on :5173, proxying /api to :7788
pnpm build    # writes dist/
pnpm check    # svelte-check

Run the Go server on :7788 (make run from the repo root) and pnpm dev proxies API and health requests to it, so cookies and sessions behave exactly as they will in production.

Layout

src/
  main.ts                 entry point
  App.svelte              login gate -> shell
  app.css                 design tokens and base styles
  lib/
    api.ts                typed client; mirrors the Go handlers
    session.ts            auth + library stores
    nav.ts                view state
    components/
      Login.svelte
      Shell.svelte        nav + view switch
      Libraries.svelte    library list, create, scan, delete
      LibraryView.svelte  item grid with covers
      Reader.svelte       epub.js reader with progress sync
      Users.svelte        admin user management

The reader resumes from a spine index, which is why progress positions are integers: it round-trips through the polymorphic progress table without needing a text column.