Files
tvctl/PROJECT_MAP.md
T
44r0n7 59fb56558f 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

365 lines
12 KiB
Markdown

# 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`.
```rust
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
```rust
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
```rust
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
```rust
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)
```rust
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):
```json
{ "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
```toml
[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) |