Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
269 lines
18 KiB
Markdown
269 lines
18 KiB
Markdown
# Status & Roadmap
|
||
|
||
**For the v2 pillar lens feature matrix, see the dedicated [v2/features.md](../v2/features.md).**
|
||
|
||
Where the project is and the de-risked path to the full vision. Architecture is in
|
||
[architecture.md](./architecture.md); the mesh design origin is
|
||
[`../.project/history/20260608_fleet-manager-mesh-design.md`](../.project/history/20260608_fleet-manager-mesh-design.md).
|
||
|
||
## Status at a glance
|
||
|
||
| Area | Status | Note |
|
||
|---|---|---|
|
||
| App — playback (VLC / mpv / QuickTime) | ✅ Shipped | helper retired → mpv; no NFS — local players play downloads, else route to the always-on host |
|
||
| App — library browser | ✅ Shipped | central index fast-path + local `MEDIA_ROOTS` walk + registry fallback (no NFS) |
|
||
| App — downloads (search + transmission) | ✅ Shipped | via `mcp` CLI; search needs FlareSolverr |
|
||
| App — metadata enrichment + artwork | ✅ Shipped | regex parse + TMDB/IMDb/keyless + ffmpeg frame-grab |
|
||
| App — all UI (Home/Player/Library/Search/Downloads/Metadata/Adult/Devices/Logs/Settings) | ✅ Shipped | wired, no TODO/FIXME/`fatalError` debt |
|
||
| App — device registry (Devices tab) | ✅ Shipped (registry only) | `DeviceConfig`: type→services presets mapping to fleet classes, per-device load badge; duty *engine* still unbuilt (see fleet below) |
|
||
| App — adult content tab | ✅ Shipped | `porn-rotation.py` ported to `PornCollectionService`; `ENABLE_ADULT` compile flag + runtime `pornFeature` setting, concealed by default |
|
||
| App — VPN subsystem | ✅ Shipped | OVPN profile/credential stores (Keychain), controller, settings UI |
|
||
| App — offline cache + Now Playing/media keys | ✅ Shipped | `OfflineCacheController`, `NowPlayingController`, bandwidth policy |
|
||
| iOS app (`TVAnarchyiOS`) | ✅ Shipped (companion) | VLCKit player, library, downloads, remote control via HTTP bridge (default `:8787`); the bridge *server* is not in this repo's `mcp/` tree |
|
||
| Distribution (release/update) | ✅ Shipped (macOS) / 🟡 scaffolded (rest) | `tools/release.sh` → Forgejo; `tools/update.sh` resolves per-OS asset+dest (mac/linux/windows/android/iOS) via the single-source `tools/platform.sh`. Only the macOS asset can exist until the client is cross-platform (see north star) |
|
||
| Universal client (one app, all devices) | ❌ Designed | the spine: control-plane HTTP daemon on the always-on host → thin web client → wrap (Tauri/Capacitor) → retire Swift UIs; adult becomes a backend-entitled module. See "North star" |
|
||
| `governor` (`portable-net-tv`) | ✅ Shipped (single-host) | watch tracking + prefetch buffer |
|
||
| `mcp` (`tv-anarchy-mcp`) | ✅ Shipped | VLC / helper / transmission / display tools |
|
||
| `recommender` | ✅ Shipped | enrichment + local recs |
|
||
| MLX title refiner | ✅ Shipped | `LocalLLMTitleRefiner` → `media_rec/title_refiner.py` (MLX Qwen); cached, self-disabling, wired at app startup |
|
||
| `governor` → fleet orchestrator (stage 1) | ✅ Shipped | `governor/src/fleet/`: registry ingest, duty assignment, custody floor-check, zombie reaper, **re-pin actuation** (`fleet repin --apply`: rsync holder→target + recorded holdings), capacity probes (`fleet probe`: ssh df + EWMA uptime), periodic daemon (`fleet daemon`), research feed into `mcp` search |
|
||
| `peers_for` / `custodians_of` (stage 3 core) | ✅ Shipped (single-fleet) | source model + both policy gates enforced; unions fleet ∪ seedbox ∪ live DHT, provenance-tagged; served over HTTP by `fleet serve` (`/peers_for/<hash>`, bearer-token) — runnable on any node until a real broadcast host exists |
|
||
| Fleet WireGuard fabric (plane 1) | ❌ Designed | blocked on the `10.9.0.4` open question + root on each node |
|
||
| Seedbox source + off-home face | ❌ Blocked external | engine supports the class/duty today; needs an actual provisioned seedbox |
|
||
| Friend-mesh / F2F relay (stage 4) | ❌ Designed | needs other fleets to exist |
|
||
| Private-tracker source (stage 5) | ❌ Designed | gates already enforced in `peers.ts`; ships last, after F2F is battle-tested |
|
||
| Discord planes | ❌ Designed | control/QA/announce + availability bot; needs bot tokens/servers |
|
||
| Multi-identity / cross-fleet | ❌ Designed | single-fleet foundation must land first |
|
||
|
||
**Verdict:** the media-client layer is complete and production-grade. The
|
||
fleet/mesh layer now has its single-fleet core (registry → duties → custody →
|
||
reaper → `peers_for`) implemented and tested in the governor; what remains is
|
||
actuation (cross-host copies), the WG fabric, and every stage that requires
|
||
infrastructure outside this repo (seedbox, friends, Discord).
|
||
|
||
## North star — one client, every device
|
||
|
||
The goal: **one app we install on every device** — iOS, Android, macOS, Ubuntu,
|
||
Bluefin, Windows — controlling the display endpoints (Roku, smart TVs) it can't
|
||
run on, **with the adult feature as an opt-in package on entitled devices only**.
|
||
|
||
**Why today's app can't be that.** It is Swift + SwiftUI/AppKit/UIKit + VLCKit —
|
||
structurally Apple-only (29 UI files; the core leans on Observation/AppKit/
|
||
MediaPlayer). It reaches only 2 of the 6 install targets. The per-platform
|
||
branches in `tools/update.sh` (linux/windows/android) are honest scaffolding for
|
||
release assets a Swift+SwiftUI build **cannot produce**. No amount of packaging
|
||
fixes that; the *client technology* is the constraint.
|
||
|
||
**The unlock is already on the roadmap.** ~20% of `TVAnarchyCore` exists only to
|
||
reach the always-on host over ssh/Process, and most of the rest *fronts* state that truly
|
||
lives on the always-on host (the index, the watchlog, transmission, porn-rotation). Once
|
||
the control-plane HTTP daemon (below) exposes those as HTTP, the client keeps almost no logic — it
|
||
becomes **browse + control + a `<video>`**. A thin client is portable; a fat one
|
||
isn't. So the dependency order is: **server-side consolidation first, universal
|
||
client second.** They're one program, not two.
|
||
|
||
**Client decision (the one real fork).** To be genuinely *one* codebase across
|
||
all six:
|
||
- **Recommended — web/PWA, wrapped:** one TypeScript app; Tauri for
|
||
macOS/Windows/Linux installs, Capacitor for iOS/Android, raw PWA in any
|
||
browser (covers Bluefin and anything else for free). Talks only HTTP+WS to
|
||
the control-plane HTTP daemon on the always-on host. Reuses the workspace's TS toolchain (governor/mcp). Roku/TVs are
|
||
*controlled*, not install sites. **Cost:** the Swift macOS/iOS UIs become
|
||
legacy and are retired at parity — a real rewrite, paid down by the logic
|
||
moving server-side where it belongs. **Tradeoff to weigh:** native VLCKit
|
||
offline playback is best-in-class today; a PWA's offline story (service worker
|
||
+ Capacitor filesystem on mobile) is good but not equal — this is the one
|
||
capability the pivot risks.
|
||
- **Pragmatic alternative:** keep Swift for Apple (where it's excellent), ship
|
||
the web client only for the non-Apple platforms. Two clients, not one —
|
||
satisfies *reach* but not the literal "one app" goal.
|
||
|
||
**Adult as a package (honoring "some include it").** In the web-client world the
|
||
adult feature becomes a **lazily-loaded module gated by a per-device entitlement
|
||
the backend issues** — clients are byte-identical everywhere; the always-on host serves the
|
||
adult module *and* adult library rows only to entitled devices. That's strictly
|
||
stronger than today's client compile flag (an un-entitled install never holds
|
||
adult data to leak). The existing `ENABLE_ADULT` compile-strip survives as a
|
||
**store-safe variant** for the one case that needs zero adult code present
|
||
(an app-store submission) — cut from the same source via the asset-suffix
|
||
dimension the release pipeline already supports.
|
||
|
||
**Sequence (each phase ships):** 1) control-plane HTTP daemon — HTTP/WS service plane on the always-on host
|
||
(player, index, **`/media`**, verbs); 2) thin **web client** against it
|
||
(browse + control + player) reaching parity with the Swift app feature-by-
|
||
feature; 3) **wrap** (Tauri/Capacitor) + entitlement-gated adult module;
|
||
4) **retire** the Swift UIs and any intermediate bridge for client playback. The `/media` plane in (1)
|
||
is the same one the Roku channel needs — bought once.
|
||
|
||
> Open decision for the operator: commit to the web-thin-client rewrite (true
|
||
> one-app, accept the offline-playback tradeoff), or hold Swift-for-Apple +
|
||
> web-for-the-rest (reach without a single codebase). Everything before the
|
||
> client rewrite — the control-plane HTTP daemon, the `/media` plane — is correct either way, so the
|
||
> fork can be deferred until phase 2 without stalling phase 1.
|
||
|
||
### Repo-state note
|
||
|
||
The `PlumTV → TVAnarchy` rename and the helper subsystems (`governor/`, `mcp/`,
|
||
`recommender/`, `search/`, `fleet/`, `tools/`) were committed to `main` on
|
||
2026-06-09 as a series of atomic commits (`41afc1c`…`b44b5a2`). Stage 0
|
||
(hygiene) below is **done**.
|
||
|
||
## Remaining work — detail
|
||
|
||
### ~~MLX title refiner~~ — ✅ done 2026-06-09
|
||
|
||
`LocalLLMTitleRefiner` (Swift) shells into `media_rec/title_refiner.py` (MLX
|
||
Qwen 1.5B, same model as the show grouper), consulted only for degenerate
|
||
(<2-char) regex titles. Results are disk-cached
|
||
(`~/.local/state/tv-anarchy/title-refinements.json`); two consecutive
|
||
subprocess failures disable it for the session so a scan never pays repeated
|
||
timeouts when MLX is absent. Wired at app startup. (The seam itself had a
|
||
wiring bug — the raw-name fallback ran *before* the refiner check, making it
|
||
unreachable — fixed in the same change.)
|
||
|
||
### governor → fleet orchestrator — ✅ engine + read-only actuation done 2026-06-09
|
||
|
||
`governor/src/fleet/` implements stage 1: registry ingest (the app-side
|
||
`fleet.json` array is authoritative, `devices.json` the fallback), deterministic
|
||
duty assignment with the spec's invariants, the N-copy custody floor-check with
|
||
rolling-baton custodianship + re-pin planning, and the zombie reaper
|
||
(`healthy | stalled | dead`, mesh-first recovery, public re-search fallback).
|
||
CLI: `portable-net-tv fleet status|duties|custody|repin|reaper|peers|probe|daemon|serve`
|
||
(all `--json`). `reaper --apply` performs only safe idempotent transmission
|
||
nudges (reannounce/verify).
|
||
|
||
**Completed 2026-06-09 (later, parallel agent team):** the actuation layer —
|
||
`fleet repin --apply` executes re-pin plans (rsync on the target pulling from
|
||
the holder, recorded holdings feed back into the floor-check), `fleet probe`
|
||
measures disk/uptime over ssh (EWMA score; feeds custody disk-eligibility),
|
||
`fleet daemon` runs the duties→floor→reaper tick periodically for launchd, and
|
||
`fleet serve` exposes `/registry /custody /reaper /peers_for/<hash>` over HTTP
|
||
with optional bearer auth (the broadcast-host service, runnable on any node).
|
||
Research actions build real `mcp` search invocations (`fleet repin`/reaper
|
||
plans name them; execution behind apply flags). Remaining inside stage 1:
|
||
nothing — what's left is infrastructure (targets need `ssh` + `mediaRoot` in
|
||
`fleet.json` for repin to have somewhere to copy to) and the launchd plist
|
||
itself.
|
||
|
||
### fleet WireGuard fabric (plane 1) — blocked on a decision
|
||
|
||
The spec promotes a scoped fleet WG mesh (`tva0`, fleet-subnet-only
|
||
`AllowedIPs`, collision-probe) to a foundational stage, but its design is
|
||
explicitly blocked on the open question of what `10.9.0.4` is today (general
|
||
overlay vs ad-hoc tv-anarchy overlay), and bring-up needs root on every node.
|
||
Decide, then build.
|
||
|
||
### stages 2/4/5 + Discord — blocked on external infrastructure
|
||
|
||
The engine already models seedboxes (class, duty preference, source defaults)
|
||
and enforces the private-tracker gates; what doesn't exist is the
|
||
infrastructure: a provisioned seedbox, friends' fleets for F2F, private-tracker
|
||
credentials, Discord bots/servers. None of this is code in this repo until
|
||
those exist.
|
||
|
||
### Control-plane HTTP daemon on the always-on host (no ssh on the control path) — planned milestone
|
||
|
||
**This is phase 1 of the universal client** (see north star): it's the
|
||
prerequisite that thins the client enough to be portable, not merely an ssh
|
||
cleanup. ssh-as-transport is the app's load-bearing design debt: player control is a
|
||
fresh `ssh → sudo socat → /tmp/mpv.sock` pipeline per command (`MpvTarget`),
|
||
the library index is `ssh cat`, launch/stats/releases/restart are ssh-invoked
|
||
helper verbs (whose deploy drift required the `helper_sha` badge), and the
|
||
offline cache rsyncs over ssh. Consequences: every keyless client needs a proxy
|
||
(a roaming client app may need an intermediate bridge; a Roku/web client can't
|
||
ssh at all), every capable client reimplements ssh plumbing, and the `sudo
|
||
socat` hack exists purely because the far socket is root-owned. The
|
||
counter-examples are already in the stack: transmission (HTTP RPC :9091) and
|
||
the Roku's ECP are the robust integrations.
|
||
|
||
**Design:** one daemon on the always-on host — HTTP on LAN + WG overlay, bearer token:
|
||
- `/player/*` + WebSocket events — local unix-socket access to mpv (root
|
||
problem dissolves; no per-command process spawn)
|
||
- `/library/index` — replaces `ssh cat`
|
||
- `/media/<path>` (HTTP range now, on-demand HLS remux later) — the SAME media
|
||
plane the Roku channel / web client milestone needs; one investment
|
||
- `/launch`, `/stats`, `/releases`, `/restart` — the helper verbs
|
||
- `/version` — replaces helper_sha drift detection
|
||
|
||
**Migration (each phase shippable):** 1) daemon wraps the existing pieces
|
||
(systemd unit; deploy via the existing helper mechanism); 2) HTTP-based
|
||
`PlayerTarget` (MediaLaunchable over HTTP, like VLC's) preferred with ssh
|
||
fallback; 3) retire ssh paths for client playback + any intermediate bridge (clients talk directly
|
||
to the always-on host). ssh remains for deploys/admin only.
|
||
|
||
### Roku dev channel — planned milestone
|
||
|
||
The living-room display has a Roku Streaming Stick 4K (ECP)
|
||
alongside the central host's HDMI input. ECP **transport control is done** (HostKind
|
||
`.roku` / `RokuTarget` — pause/jump-back/exit/now-playing from the Devices
|
||
tab). The channel itself — a couch-native TVAnarchy UI *on the Roku* — needs:
|
||
|
||
1. **HTTP media plane on the always-on host** — Roku only plays HTTP(S) streams; today the
|
||
media is NFS/ssh. A file server plus on-demand ffmpeg remux/transcode→HLS
|
||
for the incompatible tail (mkv with FLAC/Opus audio, PGS subs). This is the
|
||
heavy piece and is useful beyond Roku (any future web/TV client).
|
||
2. **BrightScript/SceneGraph client** — library browse (central index over HTTP)
|
||
+ Video node playback. New codebase, no Swift reuse.
|
||
3. **Release pipeline** — release asset + a push-deploy step
|
||
(dev channels are uploaded TO the stick). One dev-channel
|
||
slot, persists indefinitely; dev mode enable = ECP keypress sequence.
|
||
|
||
Store publishing is out: Roku killed non-certified channels (2024) and
|
||
certification would reject the torrent surfaces regardless of `ENABLE_ADULT`.
|
||
Sideload-only, which the forge model already fits.
|
||
|
||
## Build order (de-risked; each stage independently shippable)
|
||
|
||
From the design spec. Each stage ships on its own; later stages depend on earlier.
|
||
|
||
0. ~~**Hygiene (prerequisite)**~~ — ✅ done 2026-06-09: the `PlumTV → TVAnarchy`
|
||
rename and the helper subsystems are committed on `main`.
|
||
1. ~~**Host registry + duty assignment, single fleet**~~ — ✅ done 2026-06-09
|
||
(engine): app-side registry (Devices tab, `fleet.json`) + governor-side duty
|
||
engine, custody floor-check, and reaper (`governor/src/fleet/`). Remaining
|
||
inside stage 1: re-pin *actuation* (cross-host copy execution) and running
|
||
`fleet duties`/`reaper` under launchd instead of on demand.
|
||
2. **Seedbox source + `public_swarm_face` duty.** Adds an always-on custodian;
|
||
proves the source model on the zero-risk source first. ("Add a seedbox" is the
|
||
single most important onboarding step for non-power-users — most multi-device
|
||
fleets have zero always-on nodes otherwise.)
|
||
3. ~~**`peers_for()` over local sources**~~ — ✅ core done 2026-06-09:
|
||
`fleet peers <infohash|title>` unions fleet holders ∪ seedbox ∪ live DHT
|
||
peers, provenance-tagged, with both private-tracker policy gates enforced
|
||
(`search_only` default-closed; `f2f_only` forced un-overridably). Remaining:
|
||
a broadcast host to *serve* `peers_for` to other devices (today it's a local
|
||
CLI query).
|
||
4. **Friend-mesh source + F2F relay.** Federates; the custody floor goes
|
||
cross-fleet. Multi-identity lands here.
|
||
5. **Private-tracker source, default-closed.** LAST and highest-consequence:
|
||
`swarm_isolation` forced to `f2f_only`, un-overridable; `content` share is an
|
||
account-killer for the source user. Ship only once F2F isolation is
|
||
battle-tested on safe sources.
|
||
|
||
**Parallel, any time:** ~~MLX `TitleRefiner`~~ — ✅ done 2026-06-09.
|
||
**After a working mesh:** the Discord planes — control/ops + QA/community +
|
||
release announcements (project home server) and content/availability (private mesh
|
||
+ a bot inside users' *own* servers, never the public home server). Discord is a
|
||
surface you notify, not a backbone you route through: custody/reaper/F2F run over
|
||
WireGuard regardless, and a Discord ban must not take the mesh down.
|
||
|
||
## Non-goals / explicitly deferred (not defects)
|
||
|
||
- **`BlackTVTarget` removed.** Intentional — auto-migrated at runtime to `MpvTarget`
|
||
with `CommandsConfig.blackTVDefaults(bin:)`. Helper control still works.
|
||
- **No ratio enforcement.** Resolved decision (2026-06-08): ship gift-economy,
|
||
build the `custody_capacity` knob, leave it off. First lever is visibility, not
|
||
enforcement; hard ratio is a per-group last resort. Ratio anxiety is the pain
|
||
being sold against — "minus the ratio police."
|
||
- **No fingerprint-stripping on private shares.** Resolved decision: **warn hard,
|
||
strip never** by default. Stripping re-encodes and destroys the quality that
|
||
makes a private release valuable; the friend is protected structurally by
|
||
`f2f_only`, the source by the explicit warning. Strip is a per-release opt-in
|
||
only, never auto.
|
||
- **App doesn't link `governor` or call the MCP server directly.** By design — it
|
||
shells out to the `mcp` CLI and `recommender` as subprocesses; `governor` runs
|
||
independently under launchd.
|
||
- **Watch state on the always-on host.** Intentional — local to the central host, read over SSH,
|
||
never written across NFS.
|
||
- **Watch parties** (if built) = synchronized *local* playback; Discord carries
|
||
only voice + timestamps, never streamed copyrighted video.
|