SPB Git

spb/drive Public

SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.

JavaScript 82.7% CSS 10.6% Nunjucks 3.6% Shell 1.8% SQL 1.3%
27.7 KB

# CLAUDE.mdSPB Drive · Personal Cloud Drive for Simon-Pierre Boucher

Read this entire document before writing a single line of code. This file is the single source of truth. Every architectural decision, naming convention, UI detail, and non-negotiable rule lives here. When in doubt, re-read this file. When this file conflicts with your instinct, this file wins.


# 0. Identity & Non-Negotiables

Key Value
Product name SPB Drive
Owner / sole user Simon-Pierre Boucher
Contact contact@spboucher.ai
Public domain https://drive.spboucher.ai (ngrok custom domain)
Deployment host Node m3u96b
Runtime Node.js ≥ 20, ESM only
Access model Private by default. The entire drive is behind a password login. The ONLY public surfaces are explicitly created share links.
Initial password (redacted per §0.2 — supplied via SPBDRIVE_BOOTSTRAP_PASSWORD at first boot) — seeded at first boot, stored argon2-hashed, never in code, never in the repo, never logged. Changeable from Settings.

# 0.1 THE GOLDEN RULE — Mandatory Author Header

Every single file you generate — source, config, script, style, test, migration — MUST begin with an author header. No exceptions. A file without this header is a bug.

Canonical template (adapt comment syntax per language):

js
/**
 * ─────────────────────────────────────────────
 *  SPB Drive — Personal Cloud Drive
 * ─────────────────────────────────────────────
 *  Author  : Simon-Pierre Boucher
 *  Contact : contact@spboucher.ai
 *  File    : <relative/path/filename.ext>
 *  Purpose : <one-line description>
 *  License : MIT © Simon-Pierre Boucher
 * ─────────────────────────────────────────────
 */
Language Syntax
JS / TS / CSS / SCSS /** ... */ block
Python / Bash / YAML / Dockerfile / TOML # lines
HTML / Nunjucks <!-- ... --> / {# ... #}
SQL -- lines

Provide scripts/inject-headers.mjs and scripts/check-headers.mjs; npm run check:headers must fail CI if any tracked file lacks the header.

# 0.2 Password / secret handling rules (absolute)

  • The literal bootstrap password string must appear nowhere in the codebase, config files, tests, fixtures, or logs. It is provided once via the SPBDRIVE_BOOTSTRAP_PASSWORD env var (or interactive prompt) on first boot, hashed with argon2id, stored in data/auth.json, and the env var is then ignored forever.
  • Password change flow in Settings (requires current password). Also a break-glass CLI on the server: node scripts/reset-password.mjs (interactive, local only).
  • Session secret, share-link signing key: generated randomly at first boot into data/keys.json (chmod 600).

# 1. Product Vision

SPB Drive is a self-hosted Google Drive / Dropbox replacement for one person. Three purposes:

  1. Vault: all of Simon-Pierre's files, organized in folders, uploadable from any browser, safe on m3u96b.
  2. Universal previewer: click any file → beautiful in-browser preview. Images, video, audio, PDF, Office docs, code, Markdown, CSV, archives, fonts — everything imaginable previews without downloading.
  3. Sharing machine: any file or folder → one click → clean public URL (https://drive.spboucher.ai/s/<token>) with optional expiry, password, and download limits. Recipients need no account.

Quality bar: "If a stranger receives a share link, the preview page should look like a polished commercial product."


# 2. High-Level Architecture

text
┌────────────────────────────  node m3u96b  ─────────────────────────────┐
│                                                                        │
│  ┌─────────────┐   ┌────────────────────────────────────────────────┐  │
│  │   ngrok      │   │            SPB Drive Server (Node 20)          │  │
│  │  tunnel      │──▶│  Fastify app :7430                             │  │
│  │ drive.       │   │  ├─ /login, /app/*     Web UI (SSR + JS)       │  │
│  │ spboucher.ai │   │  ├─ /api/v1/*          JSON API (session auth) │  │
│  └─────────────┘   │  ├─ /s/:token[/...]    PUBLIC share pages      │  │
│                    │  ├─ /dl/*, /stream/*   Auth'd download/stream  │  │
│                    │  └─ /thumb/*           Thumbnails              │  │
│                    └────────────────┬───────────────────────────────┘  │
│                                     │                                  │
│      ┌──────────────────────────────┼──────────────────────────────┐   │
│      │                              │                              │   │
│ ┌────▼─────────────┐   ┌────────────▼───────────┐   ┌──────────────▼─┐ │
│ │ /srv/drive/files │   │ /srv/drive/db          │   │ /srv/drive/    │ │
│ │ content store:   │   │ drive.sqlite (better-  │   │ cache/         │ │
│ │ blobs by sha256  │   │ sqlite3, WAL):         │   │ thumbnails,    │ │
│ │ /ab/cd/abcd...   │   │ nodes, shares, tags,   │   │ transcodes,    │ │
│ │ (dedup natural)  │   │ sessions, activity, FTS│   │ office→pdf     │ │
│ └──────────────────┘   └────────────────────────┘   └────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘

Principles:

  • Content-addressed blob store (/srv/drive/files/<sha256[0:2]>/<sha256[2:4]>/<sha256>): identical files stored once (free dedup); DB nodes table maps the virtual folder tree onto blobs. Deleting a node only deletes the blob when its refcount hits zero.
  • SQLite is the metadata brain (better-sqlite3, WAL mode): tables nodes (id, parent_id, name, type file/folder, blob_sha, size, mime, created, modified, starred, color, trashed_at), shares, tags, node_tags, sessions, activity, plus an FTS5 virtual table for search.
  • Everything streams. Uploads and downloads never buffer whole files in memory. Range requests supported everywhere (video seeking!).
  • Preview/transcode work is queued (tiny in-process job queue, concurrency 2) and cached in /srv/drive/cache/ keyed by blob sha — thumbnails and conversions are computed once per unique file, ever.

# 2.1 Repository layout of this project

text
spbdrive/
├── CLAUDE.md
├── README.md                    # with badges
├── package.json
├── src/
│   ├── server.mjs               # Fastify bootstrap
│   ├── config.mjs               # zod-validated env/config
│   ├── db/
│   │   ├── schema.sql           # full schema + FTS5 + indexes
│   │   └── db.mjs               # migrations, prepared statements
│   ├── auth/
│   │   ├── password.mjs         # argon2id verify/change, lockout
│   │   └── session.mjs          # cookie sessions (httpOnly, SameSite=Lax)
│   ├── storage/
│   │   ├── blobs.mjs            # CAS store: put(stream)→sha, get, refcount GC
│   │   ├── nodes.mjs            # tree ops: mkdir, move, copy, rename, trash
│   │   └── upload.mjs           # chunked/resumable upload endpoint
│   ├── preview/
│   │   ├── router.mjs           # mime → preview strategy dispatcher
│   │   ├── thumbs.mjs           # sharp: image thumbs; ffmpeg: video poster
│   │   ├── transcode.mjs        # ffmpeg audio/video web-safe transcodes
│   │   ├── office.mjs           # libreoffice --headless → PDF conversion
│   │   ├── code.mjs             # shiki highlighting + markdown-it (GFM)
│   │   └── archive.mjs          # zip/tar listing (and inner-file preview)
│   ├── shares/
│   │   └── shares.mjs           # tokens, expiry, passwords, limits, zip-of-folder
│   ├── search/
│   │   └── search.mjs           # FTS5 queries + filters
│   ├── api/v1.mjs
│   └── web/
│       ├── routes.mjs
│       ├── views/               # Nunjucks templates (login, app shell, share)
│       └── assets/              # tokens.css, app.css, app.js, fonts, icons
├── cli/
│   ├── spbdrive.mjs             # bin entry
│   └── commands/
├── deploy/
│   ├── ngrok.yml
│   ├── ecosystem.config.cjs     # pm2: server + tunnel
│   ├── spbdrive.service         # systemd alternative
│   ├── setup-m3u96b.sh          # idempotent bootstrap (installs ffmpeg, libreoffice)
│   └── backup.sh
├── scripts/
│   ├── inject-headers.mjs
│   ├── check-headers.mjs
│   └── reset-password.mjs
└── test/                        # vitest unit + e2e (upload→preview→share round-trip)

# 3. Authentication & Sessions

  • Login page at /login: centered card, SPB monogram, single password field, "Remember me for 30 days" checkbox. Sharp, minimal, dark.
  • Verify with argon2id. Brute-force protection: 5 failed attempts → 15-minute lockout per IP + global exponential backoff; every failure logged to activity.
  • Session = random 256-bit id in sessions table; cookie spbdrive_sid, httpOnly, Secure, SameSite=Lax; 24 h TTL (30 d with remember-me), sliding renewal.
  • Every route except /login, /s/* (shares), /healthz, and static assets requires a valid session → otherwise 302 to /login?next=....
  • Settings page: change password, view/revoke active sessions (device + last seen), toggle theme default.

# 4. File Management Core

# 4.1 Uploads (must feel instant and bulletproof)

  • Drag & drop anywhere in the app (full-window drop overlay), plus an Upload button (files and folders — use webkitdirectory and DataTransferItem tree walking so entire folder structures upload with hierarchy preserved).
  • Chunked + resumable: client slices files into 8 MB chunks, POST /api/v1/upload/init → uploadId, PUT /api/v1/upload/:id/chunk/:n, POST /api/v1/upload/:id/complete (server assembles, sha256-streams into blob store, creates node). Interrupted uploads resume by asking the server which chunks it has.
  • Upload panel (bottom-right, Google-Drive-style): per-file progress bars, speed, cancel, retry, aggregate progress, minimize.
  • Paste-to-upload (Ctrl+V an image/screenshot into the app → lands in the current folder as pasted-YYYYMMDD-HHmmss.png).
  • No file-size limit by design; test with a multi-GB file.

# 4.2 Folder tree & organization ("groupe folder etc.")

  • Unlimited nested folders. Left sidebar: collapsible folder tree + quick sections Recent, Starred, Shared, Trash, per-tag views, and a storage usage meter (used space, per-type breakdown donut).
  • Folder colors (8 palette choices) and emoji/icon per folder.
  • Tags/labels: create colored tags, assign to any file/folder, filter by tag.
  • Starred/favorites (toggle with s).
  • Operations, all with multi-select (click, shift-click ranges, ctrl-click, drag-rectangle select): move (drag & drop onto folders or via dialog with tree picker), copy, rename (F2, inline), delete → Trash, restore, delete forever, download (multi-select → server-zips on the fly), duplicate.
  • Trash: soft delete with original-path memory; auto-purge after 30 days (daily job); "Empty trash" with typed confirmation.
  • Breadcrumb path with drag-onto-crumb to move; right-click context menu everywhere (custom-rendered, keyboard accessible).
  • Conflict handling on move/upload: "Keep both (name-2)", "Replace", "Skip" — batch-applicable.

# 4.3 Views

  • Grid view (thumbnail cards, 5 sizes via slider) and List view (name, size, type, modified, tags) — toggle persisted per folder.
  • Sort: name / size / modified / type, asc-desc. Folders always first.
  • Keyboard-first: arrows navigate, Enter opens, Space = quick-look preview overlay, Del = trash, Ctrl+A select all, / focuses search, ? shows a shortcuts cheat-sheet modal.

# 5. Universal Preview Engine — "all types imaginable" (Critical)

Clicking a file opens the Preview overlay: full-screen modal, dark scrim, filename + size header, actions (Download · Share · Star · Info · Delete), ←/→ arrows to flip through siblings, Esc closes. Every strategy below is dispatched by preview/router.mjs from MIME + extension:

Category Formats Preview behavior
Images jpg, png, gif, webp, avif, svg, bmp, ico, heic* Zoom (wheel/pinch), pan, rotate, 1:1 toggle, EXIF panel (dimensions, camera, date, GPS→"open map" link). HEIC converted to jpg via sharp if supported, else download card. Animated gif/webp play. SVG sandboxed (served with strict CSP, no scripts).
Video mp4, webm, mov, mkv, avi Native <video> with Range streaming (instant seek). Poster frame + duration badge on thumbnails (ffmpeg). Non-web-safe codecs (mkv/avi/hevc) → background ffmpeg transcode to H.264/AAC mp4, cached by blob sha; UI shows "Optimizing for playback…" with progress, then plays. Playback speed control, PiP, keyboard (space, ←→ 5 s, ↑↓ volume, f fullscreen).
Audio mp3, wav, flac, ogg, m4a, aac Custom player: waveform (wavesurfer.js or pre-computed peaks via ffmpeg), ID3 tags + embedded cover art displayed, loop, speed.
PDF Rendered with pdf.js: page thumbnails rail, page nav, zoom, text selection + in-document search, print.
Office docx, xlsx, pptx, odt, ods, odp, rtf Converted to PDF via libreoffice --headless (queued, cached by sha) then shown in the pdf.js viewer with a note "Converted preview — download for original". xlsx additionally offers a native fast path: SheetJS → styled HTML table with sheet tabs.
Code 100+ extensions (js, ts, py, go, rs, c, cpp, java, sh, sql, …) Shiki highlighting, line numbers, wrap toggle, copy button, language auto-detect fallback.
Markdown md Full GFM render (same pipeline standard as SPB Git: tables, task lists, badges inline, heading anchors, mermaid blocks) with rendered/source toggle.
Data csv, tsv Virtualized table (fast on 100k rows), sticky header, column sort, cell search, "detected delimiter" smartness.
Structured text json, yaml, toml, xml Pretty-printed, syntax-highlighted, JSON gets a collapsible tree explorer toggle.
Notebooks ipynb Cells rendered: markdown cells via GFM, code cells highlighted, outputs (text/images) shown.
Archives zip, tar, tar.gz, 7z*, rar* Browse inside the archive: file tree with sizes, preview text/image members directly (streamed extraction of single member), "Extract to folder…" action. 7z/rar listing via 7z binary if installed, else download card.
Fonts ttf, otf, woff, woff2 Specimen page: alphabet, pangram, size slider, weight info.
Email eml Parsed headers + HTML/plain body (sanitized) + attachment list (each previewable).
Ebooks epub epub.js reader with chapters.
3D (nice-to-have) stl, glb three.js orbit viewer.
Everything else * Elegant fallback card: big type icon, filename, size, mime, sha256, "Download" button — never an ugly error.

Thumbnails (/thumb/:nodeId?size=): images via sharp (256/512, webp), videos via ffmpeg frame @10%, PDFs via pdf render of page 1, office via converted-PDF page 1, code/text via generic type icons (crisp custom SVG icon set per extension family, color-coded). All cached by (sha, size).


# 6. Sharing System — clean public URLs

Any file or folder → "Share" → modal:

  • Generates https://drive.spboucher.ai/s/<token> (token = 10-char base58, unguessable, signed).
  • Options per share: expiry (1 h / 1 d / 7 d / 30 d / never / custom date), password (argon2-hashed; public visitor gets a minimal password gate page), max downloads (counter-enforced), allow download toggle (off = preview-only, download endpoints refuse), note-to-self label.
  • File share page (public, no session): centered card with the full preview engine (same viewer as the app), file name/size, Download button, "Shared by Simon-Pierre Boucher · contact@spboucher.ai" footer. Proper Open Graph tags (thumbnail as og:image) so links unfurl nicely in iMessage/Slack/Twitter.
  • Folder share page: read-only file browser of that subtree (grid/list, previews work, per-file download) + "Download all as ZIP" (server streams a zip built on the fly, archiver).
  • Share manager (app section): table of all active shares — target, URL (copy button), visits, downloads, expiry countdown, revoke button, edit options. Expired/revoked links show a clean "This link has expired" page.
  • Every share visit/download logged to activity (timestamp, IP, user-agent) and visible in the share's detail drawer.
  • QR code button next to every share URL (generated server-side, SVG).

# 7. Search, Recents & Activity

  • Global search (/ shortcut): FTS5 over name + tags + extracted text. Text extraction pipeline (queued, cached): plain/code/md/csv indexed directly; PDF via pdftotext; docx/xlsx/pptx via converted text; results ranked, with snippet highlights.
  • Filter chips in search: type (image/video/audio/doc/archive), tag, folder scope, date range, size range, starred, shared.
  • Recent view: last 50 touched files. Activity page: chronological log (uploads, renames, moves, shares created, share visits, logins) with icons — this doubles as a security audit trail.

# 8. Web UI Design Spec — must be sharp

  • Same design DNA as SPB Git (they're siblings): dark default #0b0e14 background, surface #11151c, border #1f2530, text #e6e9ef, muted #8b93a3, accent #4f8cff, accent-2 #22d3aa, danger #ff5d5d; light theme toggle; Inter + JetBrains Mono, self-hosted woff2; radius 10px; borders over shadows.
  • App shell: top bar (logo "SPB Drive", global search, upload button, view toggle, theme, settings) · left sidebar (New button with dropdown: folder/upload/paste, tree, sections, tags, storage meter) · main pane (breadcrumb, toolbar, content) · right Info panel (slides in: preview thumb, metadata, tags editor, share list, activity for that node).
  • Micro-interactions: 150 ms ease transitions, skeleton loaders, optimistic UI on rename/move/star with rollback on error, toast notifications (undo on trash: "Moved to trash · Undo").
  • Empty states illustrated (custom minimal SVG art): empty folder, empty trash, no search results.
  • Fully responsive: on mobile, sidebar becomes a drawer, grid adapts, upload via native picker, previews go full-screen. Touch: long-press = context menu.
  • Custom 404/500/expired-share pages in the design system. Favicon + PWA manifest (installable, standalone display) — offline is out of scope, but the icon on a phone home screen must look pro.
  • Lighthouse ≥ 90/95 (app) and ≥ 95/95 (share pages). No third-party CDN requests anywhere.

# 9. spbdrive CLI

Companion CLI (npm i -g / npm link), config in ~/.spbdrive/config.json (server URL + an API token created in Settings — the CLI never stores the login password). Chmod 600.

Command Behavior
spbdrive init Wizard: server URL + API token paste, verifies with /api/v1/me.
spbdrive ls [remote-path] List a folder (aligned, colorized; --json).
spbdrive up <files...> [-d /remote/folder] Chunked upload with progress bars; directories recurse.
spbdrive down <remote-path> [local] Download file, or folder as zip.
spbdrive mkdir / mv / rm / restore Tree operations (rm → trash).
spbdrive share <remote-path> [--expires 7d] [--password] [--max-dl N] Create share, print URL (and --qr renders an ANSI QR in the terminal).
spbdrive shares / spbdrive revoke <token> Manage shares.
spbdrive search "query" FTS search from terminal.
spbdrive push <local-dir> <remote-dir> One-way sync mirror (hash-compare, upload changed, --delete optional).
spbdrive doctor Config/token/server/ffmpeg-on-server diagnostics.

Exit codes 0/1/2/3 and NO_COLOR respected, same conventions as spbgit.


# 10. JSON API (/api/v1) — session or Bearer API-token auth

GET /me · GET /nodes/:id · GET /nodes/:id/children?sort&view · POST /nodes (mkdir) · PATCH /nodes/:id (rename/move/star/color/tags) · DELETE /nodes/:id (trash) + /restore + ?force=true · upload trio (/upload/init|chunk|complete) · GET /dl/:id + GET /stream/:id (Range) · GET /thumb/:id · shares CRUD (/shares) · GET /search?q&filters · GET /activity · GET /stats (storage totals) · POST /auth/login|logout, POST /auth/password, GET|DELETE /auth/sessions, POST /auth/api-tokens.

Errors as { "error": { "code", "message" } }. Rate limit /auth/login hard (see §3) and public /s/* endpoints (100 req/min/IP).


# 11. Deployment — node m3u96b + ngrok + drive.spboucher.ai

# 11.1 deploy/setup-m3u96b.sh (idempotent)

  1. Install Node 20, ffmpeg, libreoffice (headless), poppler-utils (pdftotext), 7z, ngrok.
  2. Create /srv/drive/{files,db,cache,backups,logs} with correct ownership/permissions (700).
  3. Clone/pull repo to /srv/drive/app, npm ci --omit=dev, run migrations.
  4. First-boot: prompt for bootstrap password (or read SPBDRIVE_BOOTSTRAP_PASSWORD), hash, store; generate session/share keys.
  5. Install pm2 config, start, pm2 save.

# 11.2 ngrok (deploy/ngrok.yml)

yaml
version: 3
agent:
  authtoken: ${NGROK_AUTHTOKEN}
endpoints:
  - name: spbdrive
    url: https://drive.spboucher.ai
    upstream:
      url: http://127.0.0.1:7430

Document: register drive.spboucher.ai in the ngrok dashboard, add the CNAME at the DNS provider, TLS terminates at ngrok, app runs trustProxy: true and reads client IP from x-forwarded-for (used by lockout + share logs).

# 11.3 Process management & ops

  • pm2 apps spbdrive-server + spbdrive-tunnel, autorestart, logs to /srv/drive/logs/, pm2 startup documented (survives m3u96b reboots). systemd unit provided as alternative.
  • GET /healthz: uptime, node count, storage used, queue depth.
  • Backups (deploy/backup.sh, nightly cron): sqlite .backup snapshot + incremental rsync-style hardlink copy of /srv/drive/files into backups/YYYY-MM-DD/, keep 14 dailies + 8 weeklies; verify + log. Restore procedure documented in README.
  • Daily maintenance job: purge 30-day trash, GC orphaned blobs (refcount 0), prune expired shares/sessions, vacuum FTS.

# 12. Security Checklist (verify each before "done")

  • The bootstrap password literal appears nowhere in the repo; grep in CI (check:secrets script) proves it.
  • argon2id everywhere (login, share passwords); constant-time token compares; lockout works (test it).
  • All cookies httpOnly + Secure + SameSite=Lax; CSRF token on state-changing form posts.
  • Share tokens ≥ 58 bits entropy; revocation immediate; expired links leak nothing (no filename in error page title).
  • Path traversal impossible (nodes are DB ids, blobs are hashes — never trust client paths; validate names against ^[^/\\\0]{1,255}$, reject ./..).
  • Uploaded HTML/SVG never served same-origin as executable content: previews sandboxed (Content-Security-Policy: sandbox, X-Content-Type-Options: nosniff; raw HTML downloads as attachment).
  • Strict CSP on the app (default-src 'self'); share pages likewise.
  • ffmpeg/libreoffice invoked with execFile (no shell), timeouts, and memory/size guards; conversion of hostile files can't take the server down.
  • Session revocation page works; logout everywhere button.
  • No secrets/passwords in logs (pino redaction paths configured).

# 13. Quality Bar & Definition of Done

Engineering standards: ESM, small modules, JSDoc on exports, prepared statements only (no string-built SQL), vitest coverage on blob store + tree ops + share logic, and an e2e test that boots the server, logs in, chunk-uploads a file, fetches its thumbnail, creates a password-protected share, and downloads through it. npm run lint + check:headers + check:secrets all green.

The platform is DONE when every box is checked:

  • bash deploy/setup-m3u96b.sh on a fresh m3u96b brings https://drive.spboucher.ai live and it survives a reboot.
  • Wrong password 5× → locked out; right password → in; password changeable in Settings.
  • Drag-dropping a folder with 500 nested files uploads with hierarchy intact, resumable, with a live progress panel.
  • A 2 GB video uploads, gets a poster thumbnail, and seeks instantly in the player; an mkv transcodes then plays.
  • docx, xlsx, pptx, pdf, epub, ipynb, zip (browse inside!), csv (100k rows), json, md (with badges), mp3 (waveform), heic, svg, and an unknown .xyz file all preview correctly or fall back elegantly.
  • Any file/folder → share URL with expiry + password + max-downloads; folder share offers ZIP-all; link unfurls with a thumbnail in social apps; revoke kills it instantly.
  • Multi-select drag-move, rename inline, trash + undo toast, restore, tags, folder colors, starred — all functional with keyboard shortcuts.
  • Search finds a word inside an uploaded PDF.
  • Storage meter, activity log, share visit logs all accurate.
  • spbdrive up ~/photos -d /Photos and spbdrive share /Photos --expires 7d work end-to-end.
  • Every generated file carries the Simon-Pierre Boucher / contact@spboucher.ai header (check:headers passes).
  • Nightly backup ran and the documented restore procedure was tested once.

# 14. Build Order (follow this sequence)

  1. Skeleton: config, Fastify, sqlite schema + migrations, logging, healthz, header/secret check scripts.
  2. Auth: password bootstrap, sessions, login page, lockout, settings (change password, sessions list).
  3. Storage core: blob store, nodes tree ops, chunked resumable upload, download/stream with Range, trash + GC.
  4. Web app shell: design tokens, layout, folder browsing (grid+list), drag-drop upload panel, multi-select ops, context menus, keyboard nav.
  5. Preview engine: thumbnails → images/video/audio → pdf.js → code/md/csv/json → office conversion → archives → fallbacks.
  6. Sharing: tokens, options, public pages (file + folder + zip-all), share manager, OG unfurls, QR.
  7. Search & activity: FTS, extraction pipeline, recents, activity log.
  8. CLI: init → ls/up/down → share → push sync → doctor.
  9. Polish: info panel, tags/colors/stars, empty states, PWA manifest, a11y, Lighthouse.
  10. Deploy: setup script, ngrok, pm2, backups + restore drill on m3u96b; run the full DoD checklist.

Work in small conventional commits (feat:, fix:, chore:). At the end of each phase, state which DoD boxes are now satisfied.