Files
tvctl/PROJECT_MAP.md
T
44r0n7 000d97fdeb feat: ship HTTP dashboard and harden daemon/API flows
Add the static HTTP dashboard example and wire in the recent daemon/API polish:
CORS-aware API routing, service-install behavior cleanup, safer systemd unit
ExecStart quoting, and friendly-name validation for path-safe targeting.

Also refresh README/API/roadmap docs, remove the temporary claude observations
file, and include the related tests for API/status and daemon validation.
2026-04-18 16:45:12 -04:00

12 KiB

PROJECT_MAP.md

tvctl — Living Project Map

This file is the single source of truth for any agent working on this project.

Read this first. Update this when you make structural changes.

Last updated: 2026-04-18


What This Project Is

tvctl is a Rust CLI tool and daemon (tvctld) that provides a unified, brand-agnostic control layer for smart TVs. It exposes a CLI and a local HTTP API so that technical users and tool builders can control any supported TV without caring about the underlying platform.

One sentence: A local-first daemon and CLI that lets technical users and tool builders script and control smart TVs through a stable, brand-agnostic API.


Project Status

Phase: Milestone 6 in progress. Daemon core, CLI, and /v1 HTTP API are complete for v1 Roku control. Remaining work is release prep (service verification, cross-compile, CI, first release). Platform v1: Roku only (via ECP HTTP API) Language: Rust Crate type: Binary (single binary distribution target)


Directory Structure

tvctl/
├── .gitignore              ← Repository hygiene and runtime artifact exclusions
├── AGENTS.md               ← Compatibility shim pointing to AGENT.md
├── README.md               ← User-facing documentation (complete)
├── PROJECT_MAP.md          ← This file. Agent context. Always read first.
├── AGENT.md                ← Instructions for AI agents working on this project
├── ROADMAP.md              ← Feature roadmap and milestone tracking
├── Cargo.toml              ← Crate manifest and dependency definitions
├── Cargo.lock              ← Locked dependency graph
├── docs/
│   ├── DESIGN.md           ← Full design decisions and rationale
│   ├── API.md              ← HTTP API specification (detailed)
│   └── ADAPTER.md          ← Adapter trait spec and implementation guide
├── examples/
│   └── http-dashboard/     ← Static browser dashboard for /v1 API endpoint testing
├── src/
│   ├── lib.rs              ← Library surface for shared modules and integration tests
│   ├── main.rs             ← Binary entry point and runtime bootstrap
│   ├── logging.rs          ← Tracing initialization and runtime log-filter reload
│   ├── cli/                ← CLI layer (clap-based scaffold)
│   │   └── mod.rs
│   ├── daemon/             ← tvctld daemon core scaffolding
│   │   ├── mod.rs
│   │   ├── config.rs       ← Config loading and runtime path helpers
│   │   ├── ipc.rs          ← Unix socket request/response protocol
│   │   ├── registry.rs     ← Device registry
│   │   ├── discovery.rs    ← SSDP discovery service
│   │   ├── cache.rs        ← App cache manager
│   │   └── state.rs        ← State cache
│   ├── api/                ← HTTP API server scaffold (axum)
│   │   └── mod.rs
│   └── adapters/           ← Platform adapters and shared types
│       ├── mod.rs          ← Adapter trait definition and core data shapes
│       └── roku/           ← Roku ECP adapter implementation
│           └── mod.rs
├── tests/
│   └── roku_live.rs        ← Live Roku integration tests gated by env vars
└── cache/                  ← Runtime cache (gitignored)

Core Design Decisions

These are final. Do not re-open without explicit instruction.

Decision Choice Reason
Language Rust Developer familiarity, single binary, async ecosystem
CLI framework clap Best-in-class for Rust, shell completions, polished help
HTTP framework axum Mature, tokio-native, clean routing
CLI↔daemon transport Unix socket Secure, no port conflicts, user-only access
Tool builder transport HTTP API Stable, versioned, JSON
Config format TOML Rust ecosystem standard, human-readable
CLI pattern resource-verb Consistent, mirrors REST, scales well
API versioning /v1/ from day one Stability signal to tool builders
App cache strategy Organic growth from live TV data Accurate, small, zero maintenance
State cache In-memory only, cleared on restart Honest — never pretend to have stale state
Device registry Persisted to devices.json Survives restarts
Default HTTP bind 127.0.0.1 Secure by default
Key naming kebab-case normalized Unix convention, adapter handles translation
Systemd User-level service, not system Runs as user, not root

Architecture

CLI (tvctl)
  └── Unix Socket
        └── tvctld daemon
              ├── Core Router
              ├── Device Registry (devices.json)
              ├── App Cache (cache/{platform}.apps.json)
              ├── State Cache (in-memory)
              ├── Discovery Service (SSDP)
              ├── Adapter Registry
              │     └── Roku Adapter (ECP/HTTP)
              └── HTTP API Server (:7272)
                    └── /v1/... (tool builders)

The CLI reaches the daemon over the Unix socket. The HTTP API runs in the same process and dispatches directly into the daemon request executor instead of looping back through the socket layer.


Adapter Trait

The central abstraction. All platform implementations must satisfy this trait. Defined in src/adapters/mod.rs.

pub trait TvAdapter: Send + Sync {
    async fn discover(&self) -> Result<Vec<DeviceInfo>>;
    async fn state(&self, device: &Device) -> Result<DeviceState>;
    async fn launch(&self, device: &Device, app: &str) -> Result<()>;
    async fn stop_app(&self, device: &Device) -> Result<()>;
    async fn key(&self, device: &Device, key: TvKey) -> Result<()>;
    async fn sequence(&self, device: &Device, keys: Vec<TvKey>) -> Result<()>;
    async fn list_apps(&self, device: &Device) -> Result<Vec<AppInfo>>;
    async fn dev_install(&self, device: &Device, zip: &[u8]) -> Result<()> {
        Err(TvError::NotSupported("dev_install"))
    }
    async fn dev_reload(&self, device: &Device) -> Result<()> {
        Err(TvError::NotSupported("dev_reload"))
    }
    async fn dev_logs(&self, device: &Device) -> Result<Vec<String>> {
        Err(TvError::NotSupported("dev_logs"))
    }
}

Dev methods have default implementations that return NotSupported. Platforms that support them override. Nothing above the adapter layer needs to know which platforms support which features.


Data Shapes

Device

pub struct Device {
    pub id: Uuid,
    pub name: String,           // user-assigned friendly name
    pub original_name: String,  // name as reported by device at discovery
    pub platform: String,       // "roku" | "googletv" | "firetv"
    pub address: IpAddr,
    pub port: u16,
    pub is_default: bool,
    pub discovered_at: DateTime<Utc>,
    pub last_seen: DateTime<Utc>,
}

DeviceState

pub struct DeviceState {
    pub device_id: Uuid,
    pub power: PowerState,      // On | Off | Unknown
    pub active_app: Option<AppInfo>,
    pub volume: Option<VolumeInfo>,
    pub timestamp: DateTime<Utc>,
}

AppInfo

pub struct AppInfo {
    pub id: String,             // tvctl normalized id
    pub name: String,
    pub version: Option<String>,
    pub platform_id: String,    // raw platform value
}

TvKey (normalized key enum)

pub enum TvKey {
    Home, Back, Up, Down, Left, Right, Select,
    Play, Pause, PlayPause, Stop, Rewind, FastForward, Replay, Skip,
    ChannelUp, ChannelDown,
    VolumeUp, VolumeDown, Mute,
    Power, PowerOn, PowerOff,
    InputHdmi1, InputHdmi2, InputHdmi3, InputHdmi4, InputAv, InputTuner,
    Search, Info, Options,
    Literal(String),  // for text input
}

CLI Command Tree

tvctl
├── (bare)              full help output
├── daemon
│   ├── start
│   ├── stop
│   ├── restart
│   ├── status
│   ├── install         generate + enable systemd user service
│   └── uninstall
├── device
│   ├── list
│   ├── discover
│   ├── add             --address <ip> --platform <platform>
│   ├── select <name>
│   ├── info <name>
│   └── remove <name>
├── app
│   ├── list
│   ├── launch <name|id>
│   ├── stop
│   └── refresh
├── remote
│   └── send <key> [key...]
├── state
├── dev
│   ├── install <zip>
│   ├── reload
│   └── logs
└── config
    ├── list
    ├── get <key>
    ├── set <key> <value>
    ├── reset
    └── reload

Global flags: --device <name|id>  --json  --help  --version

HTTP API Surface

Base: http://127.0.0.1:7272/v1

GET    /v1/devices
POST   /v1/devices/discover
GET    /v1/devices/{id}
DELETE /v1/devices/{id}
GET    /v1/devices/{id}/state
GET    /v1/devices/{id}/apps
POST   /v1/devices/{id}/apps/launch        body: {"app": "netflix"}
POST   /v1/devices/{id}/apps/stop
POST   /v1/devices/{id}/apps/refresh
POST   /v1/devices/{id}/remote/key         body: {"key": "home"}
POST   /v1/devices/{id}/remote/sequence    body: {"keys": ["home", "down"]}
POST   /v1/devices/{id}/dev/install        body: multipart zip
POST   /v1/devices/{id}/dev/reload
GET    /v1/devices/{id}/dev/logs
GET    /v1/daemon/status
GET    /v1/config
PATCH  /v1/config
POST   /v1/config/reload

Response envelope (always):

{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "hint": "..." } }

Storage Layout

~/.config/tvctl/config.toml         ← user config (TOML)
~/.local/share/tvctl/devices.json   ← device registry (daemon managed)
~/.local/share/tvctl/cache/
    roku.apps.json                  ← organic app cache per platform
    googletv.apps.json
    firetv.apps.json
/run/user/{uid}/tvctl.sock          ← unix socket (runtime)

Config Schema

[daemon]
socket = "/run/user/1000/tvctl.sock"
http_enabled = true
http_port = 7272
http_host = "127.0.0.1"
log_level = "info"

[discovery]
auto_discover = true
interval_secs = 300
timeout_secs = 5

[devices]
default = ""

[dev]
enabled = true
roku_username = ""
roku_password = ""

Platform Support

Platform Status Protocol
Roku v1 target ECP (HTTP on port 8060)
Google TV post-MVP ADB
Fire TV post-MVP ADB
Samsung deprioritized Tizen (unstable app IDs)

Known Risks

Risk Severity Mitigation
Roku ECP changes without notice High/Low-likelihood Adapter isolation
SSDP blocked on some networks Medium Manual device add fallback
App name resolution ambiguity Low Raw --id flag fallback
State staleness Low Honest timestamps on all state
Unix socket permissions Medium 0600, user-level systemd service
Platform creep Medium Roku-first discipline in v1

What Has NOT Been Started

  • CI/CD configuration
    • Must pin Rust toolchain >= 1.85 because edition = "2024" is in use.
  • Cross-compile validation for x86_64 and aarch64
  • Release/packaging

Glossary

Term Meaning
ECP External Control Protocol — Roku's HTTP control API on port 8060
SSDP Simple Service Discovery Protocol — used for device discovery on LAN
Adapter A platform-specific implementation of the TvAdapter trait
Registry The persisted list of known devices (devices.json)
Platform cache Per-platform app list cache (grows from live TV data)
Device cache Per-device installed app list (subset of platform cache)