Files
k-tv/docs/superpowers/plans/2026-07-12-backend-restructure.md

1387 lines
46 KiB
Markdown

# K-TV Backend Restructure Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Restructure k-tv-backend from 3-crate layout (domain/infra/api) to proper DDD hexagonal architecture with `crates/` directory, matching movies-diary reference project.
**Architecture:** Bottom-up migration — build new crates under `/mnt/drive/dev/k-tv/crates/`, layer by layer: domain → application → api-types → infra-wiring → adapters → presentation → mcp. Old `k-tv-backend/` stays untouched as reference. Each task must compile before moving to the next.
**Tech Stack:** Rust (edition 2024), Axum 0.8, SQLx, Tokio, Serde, chrono-tz, utoipa (OpenAPI), async-trait, thiserror, uuid
## Global Constraints
- Rust edition 2024 for all crates
- No `k-core` dependency — inline what's needed
- Domain crate: zero I/O deps (no sqlx, no reqwest, no tokio runtime)
- All entity fields private with `new()`, `from_persistence()`, getters
- ID types are newtypes via `uuid_id!` macro (not type aliases)
- Ports split CQRS: separate Command (write) and Query (read) traits
- Application use cases: free `async fn execute(deps, cmd)` functions
- Test doubles (InMemory + Noops) in domain behind `test-helpers` feature
- Tests in sibling `tests/` directories via `#[cfg(test)] #[path = "tests/foo.rs"] mod tests;`
- API types use `utoipa::ToSchema` for OpenAPI generation
- Reference code lives at `/mnt/drive/dev/k-tv/k-tv-backend/` — read it, don't modify it
- Reference architecture lives at `/mnt/drive/dev/movies-diary/` — follow its patterns
- When creating a new crate, add it to `[workspace] members` in root `Cargo.toml`
- Old `k-tv-backend/` has its own `Cargo.toml` workspace — do NOT conflict with the new root workspace
---
### Task 1: Workspace scaffold + domain foundation (errors, uuid_id macro, ID types)
**Files:**
- Create: `Cargo.toml` (workspace root at `/mnt/drive/dev/k-tv/Cargo.toml`)
- Create: `crates/domain/Cargo.toml`
- Create: `crates/domain/src/lib.rs`
- Create: `crates/domain/src/errors/mod.rs`
- Create: `crates/domain/src/value_objects/mod.rs`
- Create: `crates/domain/src/value_objects/ids.rs`
**Interfaces:**
- Produces: `DomainError`, `DomainResult<T>`, `uuid_id!` macro, `UserId`, `ChannelId`, `SlotId`, `BlockId`, `ScheduleId`, `MediaItemId`
- [ ] **Step 1: Create workspace root Cargo.toml**
Create `/mnt/drive/dev/k-tv/Cargo.toml`. Note: start with only `crates/domain` as a member. Add other members as their crates are created in later tasks. The glob `crates/adapters/*` only works once adapter dirs exist.
```toml
[workspace]
members = ["crates/domain"]
exclude = ["k-tv-backend", "k-tv-frontend"]
resolver = "2"
[workspace.dependencies]
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = { version = "0.10", features = ["serde"] }
email_address = "0.2"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
url = { version = "2.5", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "macros", "chrono", "uuid"] }
axum = { version = "0.8" }
axum-extra = { version = "0.10" }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
reqwest = { version = "0.12", features = ["json"] }
utoipa = { version = "5", features = ["chrono", "uuid"] }
jsonwebtoken = "9"
# Internal crates
domain = { path = "crates/domain" }
application = { path = "crates/application" }
api-types = { path = "crates/api-types" }
infra-wiring = { path = "crates/infra-wiring" }
adapter-common = { path = "crates/adapters/adapter-common" }
adapter-sqlite = { path = "crates/adapters/sqlite" }
adapter-postgres = { path = "crates/adapters/postgres" }
adapter-auth = { path = "crates/adapters/auth" }
adapter-jellyfin = { path = "crates/adapters/jellyfin" }
adapter-local-files = { path = "crates/adapters/local-files" }
adapter-event-publisher = { path = "crates/adapters/event-publisher" }
```
- [ ] **Step 2: Create domain crate Cargo.toml**
Create `crates/domain/Cargo.toml`:
```toml
[package]
name = "domain"
version = "0.1.0"
edition = "2024"
[features]
test-helpers = []
[dependencies]
async-trait = { workspace = true }
chrono = { workspace = true }
chrono-tz = { workspace = true }
email_address = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
```
- [ ] **Step 3: Create errors module**
Create `crates/domain/src/errors/mod.rs` — copy from `k-tv-backend/domain/src/errors.rs` but change `Uuid` params to use the new ID newtypes where applicable. For now, keep `Uuid` since IDs aren't defined yet:
```rust
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DomainError {
#[error("User not found: {0}")]
UserNotFound(Uuid),
#[error("User already exists: {0}")]
UserAlreadyExists(String),
#[error("Channel not found: {0}")]
ChannelNotFound(Uuid),
#[error("No active schedule for channel: {0}")]
NoActiveSchedule(Uuid),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Invalid timezone: {0}")]
TimezoneError(String),
#[error("Unauthenticated: {0}")]
Unauthenticated(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Repository error: {0}")]
RepositoryError(String),
#[error("Infrastructure error: {0}")]
InfrastructureError(String),
}
impl DomainError {
pub fn validation(message: impl Into<String>) -> Self {
Self::ValidationError(message.into())
}
pub fn unauthenticated(message: impl Into<String>) -> Self {
Self::Unauthenticated(message.into())
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::Forbidden(message.into())
}
pub fn is_not_found(&self) -> bool {
matches!(self, DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_))
}
pub fn is_conflict(&self) -> bool {
matches!(self, DomainError::UserAlreadyExists(_))
}
}
pub type DomainResult<T> = Result<T, DomainError>;
```
- [ ] **Step 4: Create uuid_id macro and ID types**
Create `crates/domain/src/value_objects/ids.rs`:
```rust
use uuid::Uuid;
macro_rules! uuid_id {
($name:ident) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct $name(Uuid);
impl $name {
pub fn generate() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn value(&self) -> Uuid {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for $name {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
impl From<Uuid> for $name {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
};
}
pub(crate) use uuid_id;
uuid_id!(UserId);
uuid_id!(ChannelId);
uuid_id!(SlotId);
uuid_id!(BlockId);
uuid_id!(ScheduleId);
/// Opaque media item identifier — format is provider-specific.
/// The domain never inspects the string; it just passes it back to the provider.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MediaItemId(String);
impl MediaItemId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn into_inner(self) -> String {
self.0
}
pub fn value(&self) -> &str {
&self.0
}
}
impl AsRef<str> for MediaItemId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for MediaItemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for MediaItemId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for MediaItemId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
```
- [ ] **Step 5: Create value_objects mod.rs**
Create `crates/domain/src/value_objects/mod.rs`:
```rust
pub mod ids;
pub use ids::*;
```
- [ ] **Step 6: Create lib.rs**
Create `crates/domain/src/lib.rs`:
```rust
pub mod errors;
pub mod value_objects;
pub use errors::{DomainError, DomainResult};
pub use value_objects::*;
```
- [ ] **Step 7: Verify it compiles**
Run: `cargo check -p domain`
Expected: compiles with no errors
- [ ] **Step 8: Commit**
```bash
git add Cargo.toml crates/domain/
git commit -m "scaffold workspace + domain foundation (errors, ids, uuid_id macro)"
```
---
### Task 2: Domain value objects (auth, scheduling, channel, oidc, search)
**Files:**
- Create: `crates/domain/src/value_objects/auth.rs`
- Create: `crates/domain/src/value_objects/scheduling.rs`
- Create: `crates/domain/src/value_objects/channel.rs`
- Create: `crates/domain/src/value_objects/oidc.rs`
- Create: `crates/domain/src/value_objects/search.rs`
- Modify: `crates/domain/src/value_objects/mod.rs`
- Modify: `crates/domain/src/errors/mod.rs`
**Interfaces:**
- Consumes: `DomainError`, `DomainResult`, ID types from Task 1
- Produces: `Email`, `Password`, `ValidationError`, `ContentType`, `MediaFilter`, `FillStrategy`, `RecyclePolicy`, `Weekday`, `AccessMode`, `LogoPosition`, OIDC newtypes, `LibrarySearchFilter`
- [ ] **Step 1: Copy and adapt auth.rs**
Copy from `k-tv-backend/domain/src/value_objects/auth.rs` into `crates/domain/src/value_objects/auth.rs`. Keep identical — `ValidationError`, `Email`, `Password` with all impls and tests.
- [ ] **Step 2: Add `From<ValidationError>` to errors**
Add to `crates/domain/src/errors/mod.rs`:
```rust
impl From<crate::value_objects::auth::ValidationError> for DomainError {
fn from(error: crate::value_objects::auth::ValidationError) -> Self {
DomainError::ValidationError(error.to_string())
}
}
```
- [ ] **Step 3: Copy and adapt scheduling.rs**
Copy from `k-tv-backend/domain/src/value_objects/scheduling.rs`. Move `MediaItemId`, `AccessMode`, and `LogoPosition` OUT (they have their own files now). Keep: `ContentType`, `MediaFilter`, `FillStrategy`, `RecyclePolicy`, `Weekday` with all impls and tests.
- [ ] **Step 4: Create channel.rs**
Create `crates/domain/src/value_objects/channel.rs` with `AccessMode` and `LogoPosition` (moved from scheduling.rs in old code):
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AccessMode {
#[default]
Public,
PasswordProtected,
AccountRequired,
OwnerOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum LogoPosition {
TopLeft,
#[default]
TopRight,
BottomLeft,
BottomRight,
}
```
- [ ] **Step 5: Copy and adapt oidc.rs**
Copy from `k-tv-backend/domain/src/value_objects/oidc.rs` into `crates/domain/src/value_objects/oidc.rs`. Keep identical — all OIDC newtypes plus `JwtSecret`.
- [ ] **Step 6: Create search.rs**
Create `crates/domain/src/value_objects/search.rs` with `LibrarySearchFilter` (moved from `library.rs` in old code since it's a value object):
```rust
use crate::value_objects::ContentType;
#[derive(Debug, Clone)]
pub struct LibrarySearchFilter {
provider_id: Option<String>,
content_type: Option<ContentType>,
series_names: Vec<String>,
collection_id: Option<String>,
genres: Vec<String>,
decade: Option<u16>,
min_duration_secs: Option<u32>,
max_duration_secs: Option<u32>,
search_term: Option<String>,
season_number: Option<u32>,
offset: u32,
limit: u32,
}
impl LibrarySearchFilter {
pub fn new() -> Self {
Self::default()
}
// Builder methods
pub fn with_provider_id(mut self, id: impl Into<String>) -> Self { self.provider_id = Some(id.into()); self }
pub fn with_content_type(mut self, ct: ContentType) -> Self { self.content_type = Some(ct); self }
pub fn with_series_names(mut self, names: Vec<String>) -> Self { self.series_names = names; self }
pub fn with_collection_id(mut self, id: impl Into<String>) -> Self { self.collection_id = Some(id.into()); self }
pub fn with_genres(mut self, genres: Vec<String>) -> Self { self.genres = genres; self }
pub fn with_decade(mut self, decade: u16) -> Self { self.decade = Some(decade); self }
pub fn with_min_duration_secs(mut self, secs: u32) -> Self { self.min_duration_secs = Some(secs); self }
pub fn with_max_duration_secs(mut self, secs: u32) -> Self { self.max_duration_secs = Some(secs); self }
pub fn with_search_term(mut self, term: impl Into<String>) -> Self { self.search_term = Some(term.into()); self }
pub fn with_season_number(mut self, n: u32) -> Self { self.season_number = Some(n); self }
pub fn with_offset(mut self, offset: u32) -> Self { self.offset = offset; self }
pub fn with_limit(mut self, limit: u32) -> Self { self.limit = limit; self }
// Getters
pub fn provider_id(&self) -> Option<&str> { self.provider_id.as_deref() }
pub fn content_type(&self) -> Option<&ContentType> { self.content_type.as_ref() }
pub fn series_names(&self) -> &[String] { &self.series_names }
pub fn collection_id(&self) -> Option<&str> { self.collection_id.as_deref() }
pub fn genres(&self) -> &[String] { &self.genres }
pub fn decade(&self) -> Option<u16> { self.decade }
pub fn min_duration_secs(&self) -> Option<u32> { self.min_duration_secs }
pub fn max_duration_secs(&self) -> Option<u32> { self.max_duration_secs }
pub fn search_term(&self) -> Option<&str> { self.search_term.as_deref() }
pub fn season_number(&self) -> Option<u32> { self.season_number }
pub fn offset(&self) -> u32 { self.offset }
pub fn limit(&self) -> u32 { self.limit }
}
impl Default for LibrarySearchFilter {
fn default() -> Self {
Self {
provider_id: None,
content_type: None,
series_names: vec![],
collection_id: None,
genres: vec![],
decade: None,
min_duration_secs: None,
max_duration_secs: None,
search_term: None,
season_number: None,
offset: 0,
limit: 50,
}
}
}
```
- [ ] **Step 7: Update value_objects/mod.rs**
```rust
pub mod auth;
pub mod channel;
pub mod ids;
pub mod oidc;
pub mod scheduling;
pub mod search;
pub use auth::*;
pub use channel::*;
pub use ids::*;
pub use oidc::*;
pub use scheduling::*;
pub use search::*;
```
- [ ] **Step 8: Update lib.rs re-exports**
Update `crates/domain/src/lib.rs` to re-export the new modules.
- [ ] **Step 9: Verify and commit**
Run: `cargo check -p domain`
Expected: compiles
```bash
git add crates/domain/
git commit -m "domain value objects: auth, scheduling, channel, oidc, search"
```
---
### Task 3: Domain models — User, Channel, ScheduleConfig, ProgrammingBlock, BlockContent, MediaItem
**Files:**
- Create: `crates/domain/src/models/mod.rs`
- Create: `crates/domain/src/models/user.rs`
- Create: `crates/domain/src/models/channel.rs`
- Create: `crates/domain/src/models/media.rs`
- Create: `crates/domain/src/models/collections.rs`
- Modify: `crates/domain/src/lib.rs`
**Interfaces:**
- Consumes: All value objects from Tasks 1-2
- Produces: `User`, `Channel`, `ScheduleConfig`, `ScheduleConfigCompat`, `OldScheduleConfig`, `ProgrammingBlock`, `BlockContent`, `MediaItem`, `PlaybackRecord`, `PageParams`, `Paginated<T>`
Key transformation: ALL fields become private. Each entity gets `new()` for creation, `from_persistence()` for DB hydration, and getter methods. Read the old `k-tv-backend/domain/src/entities.rs` for the complete field lists. Use new ID newtypes (`UserId`, `ChannelId`, etc.) instead of bare `Uuid`.
For `ScheduleConfig`, `ProgrammingBlock`, `BlockContent`: these are Serde-serialized as JSON in the DB. They need `Serialize`/`Deserialize` but fields should still be private with getters. Use `#[serde(into = "...", from = "...")]` or keep serde derives and add getters alongside. Since these are complex nested structures frequently serialized, keep `Serialize`/`Deserialize` derives and add getters.
For `MediaItem`: this is embedded in `ScheduledSlot` as JSON. Same approach — derives + getters.
Refer to `k-tv-backend/domain/src/entities.rs` for the complete type definitions. Migrate all types and tests.
- [ ] **Step 1: Create user.rs with encapsulated User**
- [ ] **Step 2: Create channel.rs with Channel, ScheduleConfig, ProgrammingBlock, BlockContent, ScheduleConfigCompat**
- [ ] **Step 3: Create media.rs with MediaItem, PlaybackRecord**
- [ ] **Step 4: Create collections.rs with PageParams, Paginated<T>**
- [ ] **Step 5: Create mod.rs with re-exports**
- [ ] **Step 6: Update lib.rs**
- [ ] **Step 7: Verify and commit**
Run: `cargo check -p domain`
```bash
git add crates/domain/
git commit -m "domain models: User, Channel, ScheduleConfig, MediaItem, PlaybackRecord"
```
---
### Task 4: Domain models — Schedule, Library, ConfigSnapshot, Activity, ProviderConfig
**Files:**
- Create: `crates/domain/src/models/schedule.rs`
- Create: `crates/domain/src/models/library.rs`
- Create: `crates/domain/src/models/config_snapshot.rs`
- Create: `crates/domain/src/models/activity.rs`
- Create: `crates/domain/src/models/provider_config.rs`
- Modify: `crates/domain/src/models/mod.rs`
**Interfaces:**
- Consumes: All value objects, `MediaItem`, `ScheduleConfig` from Tasks 1-3
- Produces: `GeneratedSchedule`, `ScheduledSlot`, `CurrentBroadcast`, `LibraryItem`, `LibraryCollection`, `LibrarySyncResult`, `LibrarySyncLogEntry`, `ShowSummary`, `SeasonSummary`, `ChannelConfigSnapshot`, `ActivityEvent`, `ProviderConfigRow`
Read old code from:
- `k-tv-backend/domain/src/entities.rs` (GeneratedSchedule, ScheduledSlot, CurrentBroadcast, ChannelConfigSnapshot)
- `k-tv-backend/domain/src/library.rs` (LibraryItem, LibraryCollection, etc.)
- `k-tv-backend/domain/src/repositories.rs` (ActivityEvent, ProviderConfigRow)
All fields private with `new()`, `from_persistence()`, getters.
- [ ] **Step 1: Create schedule.rs** — GeneratedSchedule, ScheduledSlot, CurrentBroadcast
- [ ] **Step 2: Create library.rs** — LibraryItem, LibraryCollection, LibrarySyncResult, LibrarySyncLogEntry, ShowSummary, SeasonSummary
- [ ] **Step 3: Create config_snapshot.rs** — ChannelConfigSnapshot
- [ ] **Step 4: Create activity.rs** — ActivityEvent
- [ ] **Step 5: Create provider_config.rs** — ProviderConfigRow
- [ ] **Step 6: Update mod.rs re-exports**
- [ ] **Step 7: Verify and commit**
Run: `cargo check -p domain`
```bash
git add crates/domain/
git commit -m "domain models: schedule, library, config_snapshot, activity, provider_config"
```
---
### Task 5: Domain ports — all CQRS-split trait definitions
**Files:**
- Create: `crates/domain/src/ports/mod.rs`
- Create: `crates/domain/src/ports/auth.rs`
- Create: `crates/domain/src/ports/user.rs`
- Create: `crates/domain/src/ports/channel.rs`
- Create: `crates/domain/src/ports/schedule.rs`
- Create: `crates/domain/src/ports/library.rs`
- Create: `crates/domain/src/ports/media.rs`
- Create: `crates/domain/src/ports/events.rs`
- Create: `crates/domain/src/ports/settings.rs`
- Create: `crates/domain/src/ports/activity.rs`
- Create: `crates/domain/src/ports/provider_config.rs`
- Create: `crates/domain/src/ports/transcode.rs`
- Modify: `crates/domain/src/lib.rs`
**Interfaces:**
- Consumes: All models and value objects from Tasks 1-4
- Produces: `AuthService`, `UserCommand`, `UserQuery`, `ChannelCommand`, `ChannelQuery`, `ScheduleCommand`, `ScheduleQuery`, `LibraryCommand`, `LibraryQuery`, `LibrarySyncAdapter`, `IMediaProvider`, `IProviderRegistry`, `ProviderCapabilities`, `StreamingProtocol`, `StreamQuality`, `Collection`, `SeriesSummary`, `EventPublisher`, `EventConsumer`, `EventHandler`, `AppSettingsRepository`, `ActivityLogRepository`, `ProviderConfigRepository`, `TranscodeSettingsRepository`
Read old code from:
- `k-tv-backend/domain/src/repositories.rs` — split each old trait into Command + Query
- `k-tv-backend/domain/src/ports.rs``IMediaProvider`, `IProviderRegistry`, `ProviderCapabilities`, etc.
- `k-tv-backend/domain/src/library.rs``ILibraryRepository` → split into `LibraryCommand` + `LibraryQuery`, `LibrarySyncAdapter`
CQRS split example for `UserRepository`:
```rust
// ports/user.rs
#[async_trait]
pub trait UserCommand: Send + Sync {
async fn save(&self, user: &User) -> DomainResult<()>;
async fn delete(&self, id: UserId) -> DomainResult<()>;
}
#[async_trait]
pub trait UserQuery: Send + Sync {
async fn find_by_id(&self, id: UserId) -> DomainResult<Option<User>>;
async fn find_by_subject(&self, subject: &str) -> DomainResult<Option<User>>;
async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>>;
async fn count_users(&self) -> DomainResult<u64>;
}
```
New `AuthService` port (password hashing + verification, abstracted from infra):
```rust
// ports/auth.rs
#[async_trait]
pub trait AuthService: Send + Sync {
fn hash_password(&self, password: &str) -> DomainResult<String>;
fn verify_password(&self, password: &str, hash: &str) -> DomainResult<bool>;
}
```
New event ports:
```rust
// ports/events.rs
#[async_trait]
pub trait EventPublisher: Send + Sync {
async fn publish(&self, event: DomainEvent) -> DomainResult<()>;
}
#[async_trait]
pub trait EventConsumer: Send + Sync {
async fn subscribe(&self) -> DomainResult<Box<dyn EventStream>>;
}
#[async_trait]
pub trait EventHandler: Send + Sync {
async fn handle(&self, event: &DomainEvent) -> DomainResult<()>;
}
```
`media.rs` — copy `IMediaProvider`, `IProviderRegistry`, `ProviderCapabilities`, `StreamingProtocol`, `StreamQuality`, `Collection`, `SeriesSummary` from `k-tv-backend/domain/src/ports.rs`.
- [ ] **Step 1: Create auth.rs port**
- [ ] **Step 2: Create user.rs with UserCommand + UserQuery**
- [ ] **Step 3: Create channel.rs with ChannelCommand + ChannelQuery**
- [ ] **Step 4: Create schedule.rs with ScheduleCommand + ScheduleQuery**
- [ ] **Step 5: Create library.rs with LibraryCommand + LibraryQuery + LibrarySyncAdapter**
- [ ] **Step 6: Create media.rs with IMediaProvider, IProviderRegistry, etc.**
- [ ] **Step 7: Create events.rs with EventPublisher, EventConsumer, EventHandler**
- [ ] **Step 8: Create settings.rs, activity.rs, provider_config.rs, transcode.rs**
- [ ] **Step 9: Create mod.rs with re-exports**
- [ ] **Step 10: Update lib.rs**
- [ ] **Step 11: Verify and commit**
Run: `cargo check -p domain`
```bash
git add crates/domain/
git commit -m "domain ports: CQRS-split traits for all bounded contexts"
```
---
### Task 6: Domain events
**Files:**
- Create: `crates/domain/src/events/mod.rs`
- Modify: `crates/domain/src/lib.rs`
**Interfaces:**
- Consumes: ID types, models from prior tasks
- Produces: `DomainEvent` enum
Read from `k-tv-backend/domain/src/events.rs`. Adapt to use new ID newtypes. The events should reference IDs, not full entities (keeps Clone cheap):
```rust
#[derive(Clone, Debug)]
pub enum DomainEvent {
BroadcastTransition { channel_id: ChannelId, slot_id: SlotId },
NoSignal { channel_id: ChannelId },
ScheduleGenerated { channel_id: ChannelId, schedule_id: ScheduleId },
ChannelCreated { channel_id: ChannelId },
ChannelUpdated { channel_id: ChannelId },
ChannelDeleted { channel_id: ChannelId },
}
```
- [ ] **Step 1: Create events/mod.rs**
- [ ] **Step 2: Update lib.rs**
- [ ] **Step 3: Verify and commit**
```bash
git add crates/domain/
git commit -m "domain events: DomainEvent enum"
```
---
### Task 7: Domain services — ScheduleEngineService, fill strategies, IPTV
**Files:**
- Create: `crates/domain/src/services/mod.rs`
- Create: `crates/domain/src/services/schedule/mod.rs`
- Create: `crates/domain/src/services/schedule/fill.rs`
- Create: `crates/domain/src/services/schedule/recycle.rs`
- Create: `crates/domain/src/services/iptv.rs`
- Modify: `crates/domain/src/lib.rs`
**Interfaces:**
- Consumes: All models, ports, value objects
- Produces: `ScheduleEngineService`, `fill_block()`, `apply_recycle_policy()`, `generate_m3u()`, `generate_xmltv()`
Read from:
- `k-tv-backend/domain/src/services/schedule/mod.rs` (392 lines)
- `k-tv-backend/domain/src/services/schedule/fill.rs` (151 lines)
- `k-tv-backend/domain/src/services/schedule/recycle.rs` (55 lines)
- `k-tv-backend/domain/src/iptv.rs` (93 lines)
Note: The old `UserService` and `ChannelService` are NOT migrated here — their logic moves to application use cases. Only `ScheduleEngineService` stays in domain because it contains pure scheduling algorithms.
Adapt all code to use new newtypes (getters instead of field access, `UserId` instead of `Uuid`, etc.).
- [ ] **Step 1: Create schedule/fill.rs** — adapt fill_block, fill_best_fit, fill_sequential
- [ ] **Step 2: Create schedule/recycle.rs** — adapt apply_recycle_policy
- [ ] **Step 3: Create schedule/mod.rs** — adapt ScheduleEngineService
- [ ] **Step 4: Create iptv.rs** — adapt generate_m3u, generate_xmltv
- [ ] **Step 5: Create services/mod.rs**
- [ ] **Step 6: Update lib.rs**
- [ ] **Step 7: Verify and commit**
Run: `cargo check -p domain`
```bash
git add crates/domain/
git commit -m "domain services: schedule engine, fill strategies, IPTV"
```
---
### Task 8: Domain testing — InMemory repos + Noops
**Files:**
- Create: `crates/domain/src/testing/mod.rs`
- Create: `crates/domain/src/testing/in_memory.rs`
- Create: `crates/domain/src/testing/noops.rs`
- Modify: `crates/domain/src/lib.rs`
**Interfaces:**
- Consumes: All port traits from Task 5
- Produces: `InMemoryUserRepository`, `InMemoryChannelRepository`, `InMemoryScheduleRepository`, `InMemoryLibraryRepository`, `NoopEventPublisher`, `NoopMediaProvider`, `NoopActivityLog`, etc.
InMemory repos: `Arc<Mutex<HashMap<Id, Entity>>>` implementing both Command and Query traits. One struct implements both traits (since InMemory doesn't need separate read/write stores).
Noops: return `Ok(())` for writes, `Ok(None)` / `Ok(vec![])` for reads.
Reference: `movies-diary/crates/domain/src/testing/` for pattern.
Gated behind `#[cfg(feature = "test-helpers")]` in lib.rs:
```rust
#[cfg(feature = "test-helpers")]
pub mod testing;
```
- [ ] **Step 1: Create in_memory.rs** — InMemory implementations for all ports
- [ ] **Step 2: Create noops.rs** — Noop implementations for all ports
- [ ] **Step 3: Create testing/mod.rs** — re-exports
- [ ] **Step 4: Update lib.rs** — add cfg-gated `pub mod testing`
- [ ] **Step 5: Verify and commit**
Run: `cargo check -p domain --features test-helpers`
```bash
git add crates/domain/
git commit -m "domain testing: InMemory repos + Noops behind test-helpers feature"
```
---
### Task 9: Application crate — auth bounded context
**Files:**
- Create: `crates/application/Cargo.toml`
- Create: `crates/application/src/lib.rs`
- Create: `crates/application/src/auth/mod.rs`
- Create: `crates/application/src/auth/deps.rs`
- Create: `crates/application/src/auth/commands.rs`
- Create: `crates/application/src/auth/queries.rs`
- Create: `crates/application/src/auth/register.rs`
- Create: `crates/application/src/auth/login.rs`
- Create: `crates/application/src/auth/tests/register.rs`
- Create: `crates/application/src/auth/tests/login.rs`
**Interfaces:**
- Consumes: `domain::*` (ports, models, value objects, testing)
- Produces: `AuthDeps`, `RegisterCommand`, `LoginCommand`, `auth::register::execute()`, `auth::login::execute()`
```toml
# crates/application/Cargo.toml
[package]
name = "application"
version = "0.1.0"
edition = "2024"
[dependencies]
domain = { workspace = true, features = ["test-helpers"] }
async-trait = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
```
Wait — `test-helpers` should only be enabled in dev-dependencies. Fix:
```toml
[dependencies]
domain = { workspace = true }
async-trait = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]
domain = { workspace = true, features = ["test-helpers"] }
tokio = { workspace = true }
```
Pattern for each use case file:
```rust
// auth/register.rs
use domain::*;
use super::deps::AuthDeps;
use super::commands::RegisterCommand;
pub async fn execute(deps: &AuthDeps, cmd: RegisterCommand) -> DomainResult<User> {
// validate, create entity, persist, publish event
}
#[cfg(test)]
#[path = "tests/register.rs"]
mod tests;
```
Read old logic from:
- `k-tv-backend/domain/src/services/user.rs` (UserService methods become use cases)
- `k-tv-backend/api/src/routes/auth/local.rs` (registration/login handler logic)
- [ ] **Step 1: Create application Cargo.toml**
- [ ] **Step 2: Create deps.rs** — AuthDeps struct
- [ ] **Step 3: Create commands.rs and queries.rs**
- [ ] **Step 4: Write register test**`tests/register.rs`
- [ ] **Step 5: Implement register.rs**
- [ ] **Step 6: Write login test**
- [ ] **Step 7: Implement login.rs**
- [ ] **Step 8: Create mod.rs and lib.rs**
- [ ] **Step 9: Verify and commit**
Run: `cargo test -p application`
```bash
git add crates/application/
git commit -m "application: auth bounded context (register, login)"
```
---
### Task 10: Application crate — channels bounded context
**Files:**
- Create: `crates/application/src/channels/mod.rs`
- Create: `crates/application/src/channels/deps.rs`
- Create: `crates/application/src/channels/commands.rs`
- Create: `crates/application/src/channels/queries.rs`
- Create: `crates/application/src/channels/create.rs`
- Create: `crates/application/src/channels/update.rs`
- Create: `crates/application/src/channels/delete.rs`
- Create: `crates/application/src/channels/get.rs`
- Create: `crates/application/src/channels/list.rs`
- Create: `crates/application/src/channels/list_by_owner.rs`
- Create: `crates/application/src/channels/tests/`
**Interfaces:**
- Consumes: `domain::*`
- Produces: `ChannelCommandDeps`, `ChannelQueryDeps`, `CreateChannelCommand`, `UpdateChannelCommand`, use case `execute()` functions
Read old logic from `k-tv-backend/domain/src/services/channel.rs` (ChannelService methods → use cases).
- [ ] **Step 1: Create deps.rs**
- [ ] **Step 2: Create commands.rs and queries.rs**
- [ ] **Step 3: Write and implement create.rs with tests**
- [ ] **Step 4: Write and implement update.rs with tests**
- [ ] **Step 5: Write and implement delete.rs with tests**
- [ ] **Step 6: Write and implement get.rs, list.rs, list_by_owner.rs with tests**
- [ ] **Step 7: Create mod.rs, update lib.rs**
- [ ] **Step 8: Verify and commit**
Run: `cargo test -p application`
```bash
git add crates/application/
git commit -m "application: channels bounded context"
```
---
### Task 11: Application crate — schedule bounded context
**Files:**
- Create: `crates/application/src/schedule/mod.rs`, `deps.rs`, `commands.rs`, `queries.rs`
- Create: `crates/application/src/schedule/generate.rs`, `get_active.rs`, `get_current_broadcast.rs`, `get_epg.rs`, `get_stream_url.rs`, `list_history.rs`
- Create: `crates/application/src/schedule/tests/`
**Interfaces:**
- Consumes: `domain::*`, `ScheduleEngineService`
- Produces: `ScheduleDeps`, `GenerateScheduleCommand`, schedule query functions
The schedule use cases orchestrate calling `ScheduleEngineService` (which stays in domain). Read old logic from `k-tv-backend/domain/src/services/schedule/mod.rs`.
- [ ] **Step 1-6: Create deps, commands, queries, implement all use cases with tests**
- [ ] **Step 7: Verify and commit**
```bash
git add crates/application/
git commit -m "application: schedule bounded context"
```
---
### Task 12: Application crate — library bounded context
**Files:**
- Create: `crates/application/src/library/mod.rs`, `deps.rs`, `commands.rs`, `queries.rs`
- Create: `crates/application/src/library/sync.rs`, `search.rs`, `list_collections.rs`, `list_shows.rs`, `list_seasons.rs`
- Create: `crates/application/src/library/tests/`
**Interfaces:**
- Consumes: `domain::*`
- Produces: `LibraryCommandDeps`, `LibraryQueryDeps`, `TriggerSyncCommand`, library query functions
Read old logic from `k-tv-backend/api/src/routes/library.rs` (handler logic → use cases).
- [ ] **Step 1-6: Create deps, commands, queries, implement all use cases with tests**
- [ ] **Step 7: Verify and commit**
```bash
git add crates/application/
git commit -m "application: library bounded context"
```
---
### Task 13: Application crate — remaining bounded contexts
**Files:**
- Create: `crates/application/src/config_snapshots/` — save, restore, list, patch_label
- Create: `crates/application/src/admin/` — update_settings, get_settings, activity_log
- Create: `crates/application/src/providers/` — upsert, delete, list
- Create: `crates/application/src/iptv/` — m3u, xmltv
**Interfaces:**
- Consumes: `domain::*`
- Produces: All remaining use case functions
Follow the same pattern as Tasks 9-12 for each bounded context.
- [ ] **Step 1: config_snapshots — deps, commands, queries, use cases, tests**
- [ ] **Step 2: admin — deps, commands, queries, use cases, tests**
- [ ] **Step 3: providers — deps, commands, queries, use cases, tests**
- [ ] **Step 4: iptv — deps, queries, use cases, tests**
- [ ] **Step 5: Verify and commit**
Run: `cargo test -p application`
```bash
git add crates/application/
git commit -m "application: config_snapshots, admin, providers, iptv"
```
---
### Task 14: API types crate
**Files:**
- Create: `crates/api-types/Cargo.toml`
- Create: `crates/api-types/src/lib.rs`
- Create: `crates/api-types/src/auth.rs`, `channels.rs`, `schedule.rs`, `library.rs`, `admin.rs`, `config.rs`, `providers.rs`, `transcode.rs`, `common.rs`
**Interfaces:**
- Consumes: `domain` (for types referenced in responses)
- Produces: All `*Request`, `*Response`, `*Dto` types with `utoipa::ToSchema`
```toml
# crates/api-types/Cargo.toml
[package]
name = "api-types"
version = "0.1.0"
edition = "2024"
[dependencies]
domain = { workspace = true }
serde = { workspace = true }
utoipa = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
```
Read old DTOs from `k-tv-backend/api/src/dto.rs`. Each DTO gets `#[derive(Serialize, Deserialize, utoipa::ToSchema)]`.
- [ ] **Step 1: Create Cargo.toml**
- [ ] **Step 2: Create common.rs** — PaginatedResponse<T>, ErrorResponse
- [ ] **Step 3: Create auth.rs** — LoginRequest, RegisterRequest, TokenResponse, UserResponse
- [ ] **Step 4: Create channels.rs** — CreateChannelRequest, UpdateChannelRequest, ChannelResponse
- [ ] **Step 5: Create schedule.rs, library.rs, admin.rs, config.rs, providers.rs, transcode.rs**
- [ ] **Step 6: Create lib.rs**
- [ ] **Step 7: Verify and commit**
Run: `cargo check -p api-types`
```bash
git add crates/api-types/
git commit -m "api-types: HTTP DTOs with utoipa OpenAPI derives"
```
---
### Task 15: Infra-wiring crate
**Files:**
- Create: `crates/infra-wiring/Cargo.toml`
- Create: `crates/infra-wiring/src/lib.rs`
**Interfaces:**
- Produces: `DbPool` enum, `Config` struct
```toml
# crates/infra-wiring/Cargo.toml
[package]
name = "infra-wiring"
version = "0.1.0"
edition = "2024"
[features]
default = ["sqlite"]
sqlite = ["sqlx/sqlite"]
postgres = ["sqlx/postgres"]
[dependencies]
sqlx = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
```
Read old config from `k-tv-backend/api/src/config.rs`. Migrate `Config` struct and `DbPool` enum.
- [ ] **Step 1: Create Cargo.toml**
- [ ] **Step 2: Create lib.rs** — DbPool enum, Config struct, Config::from_env()
- [ ] **Step 3: Verify and commit**
Run: `cargo check -p infra-wiring`
```bash
git add crates/infra-wiring/
git commit -m "infra-wiring: DbPool enum + Config"
```
---
### Task 16: Adapter — adapter-common
**Files:**
- Create: `crates/adapters/adapter-common/Cargo.toml`
- Create: `crates/adapters/adapter-common/src/lib.rs`
**Interfaces:**
- Consumes: `domain`
- Produces: `map_sqlx_error()`, shared row-to-domain conversion helpers
Read old mapping code from `k-tv-backend/infra/src/channel_repository/mapping.rs`, `user_repository/mapping.rs`, `schedule_repository/mapping.rs`.
- [ ] **Step 1: Create Cargo.toml**
- [ ] **Step 2: Create lib.rs** — map_sqlx_error, row mapping helpers
- [ ] **Step 3: Verify and commit**
```bash
git add crates/adapters/adapter-common/
git commit -m "adapter-common: shared DB mapping helpers"
```
---
### Task 17: Adapter — SQLite
**Files:**
- Create: `crates/adapters/sqlite/Cargo.toml`
- Create: `crates/adapters/sqlite/src/lib.rs`
- Create: `crates/adapters/sqlite/src/wire.rs`
- Create: `crates/adapters/sqlite/src/user.rs`, `channel.rs`, `schedule.rs`, `library.rs`, `activity.rs`, `settings.rs`, `provider_config.rs`, `transcode.rs`
- Copy: `k-tv-backend/migrations_sqlite/` → top-level `migrations_sqlite/`
**Interfaces:**
- Consumes: `domain`, `adapter-common`, `infra-wiring`
- Produces: `SqliteWireOutput`, all `Sqlite*Repository` types
Read old implementations from `k-tv-backend/infra/src/*_repository/sqlite.rs` files. Each repo struct wraps `SqlitePool` and implements both Command + Query traits.
`wire()` function creates pool, runs migrations, returns `SqliteWireOutput` with `Arc<dyn Port>` for each.
- [ ] **Step 1: Create Cargo.toml**
- [ ] **Step 2: Copy migrations**
- [ ] **Step 3: Implement user.rs** — SqliteUserRepository (UserCommand + UserQuery)
- [ ] **Step 4: Implement channel.rs** — SqliteChannelRepository
- [ ] **Step 5: Implement schedule.rs** — SqliteScheduleRepository
- [ ] **Step 6: Implement library.rs** — SqliteLibraryRepository
- [ ] **Step 7: Implement activity.rs, settings.rs, provider_config.rs, transcode.rs**
- [ ] **Step 8: Create wire.rs** — SqliteWireOutput, wire() function
- [ ] **Step 9: Create lib.rs**
- [ ] **Step 10: Verify and commit**
Run: `cargo check -p adapter-sqlite`
```bash
git add crates/adapters/sqlite/ migrations_sqlite/
git commit -m "adapter-sqlite: all repository implementations + wire function"
```
---
### Task 18: Adapter — PostgreSQL
**Files:**
- Create: `crates/adapters/postgres/Cargo.toml`
- Create: `crates/adapters/postgres/src/` — same structure as sqlite
**Interfaces:**
- Consumes: `domain`, `adapter-common`, `infra-wiring`
- Produces: `PostgresWireOutput`, all `Postgres*Repository` types
Read old code from `k-tv-backend/infra/src/*_repository/postgres.rs`. Note: not all repos have Postgres implementations in the old code — implement what exists, stub the rest.
- [ ] **Step 1-8: Mirror SQLite adapter for PostgreSQL**
- [ ] **Step 9: Verify and commit**
```bash
git add crates/adapters/postgres/
git commit -m "adapter-postgres: repository implementations"
```
---
### Task 19: Adapter — auth (JWT + OIDC)
**Files:**
- Create: `crates/adapters/auth/Cargo.toml`
- Create: `crates/adapters/auth/src/lib.rs`
- Create: `crates/adapters/auth/src/jwt.rs`
- Create: `crates/adapters/auth/src/oidc.rs`
- Create: `crates/adapters/auth/src/password.rs`
**Interfaces:**
- Consumes: `domain` (AuthService port)
- Produces: `JwtAuthService`, `OidcService`, `PasswordAuthService`
Read from `k-tv-backend/infra/src/auth/`. Feature-gate OIDC behind `auth-oidc`.
- [ ] **Step 1-5: Create adapter with JWT, OIDC, password hashing**
- [ ] **Step 6: Verify and commit**
```bash
git add crates/adapters/auth/
git commit -m "adapter-auth: JWT, OIDC, password hashing"
```
---
### Task 20: Adapter — Jellyfin
**Files:**
- Create: `crates/adapters/jellyfin/Cargo.toml`
- Create: `crates/adapters/jellyfin/src/lib.rs`, `config.rs`, `mapping.rs`, `models.rs`, `provider.rs`
**Interfaces:**
- Consumes: `domain` (IMediaProvider port)
- Produces: `JellyfinMediaProvider`
Read from `k-tv-backend/infra/src/jellyfin/`.
- [ ] **Step 1-5: Create Jellyfin adapter**
- [ ] **Step 6: Verify and commit**
```bash
git add crates/adapters/jellyfin/
git commit -m "adapter-jellyfin: media provider"
```
---
### Task 21: Adapter — local-files
**Files:**
- Create: `crates/adapters/local-files/Cargo.toml`
- Create: `crates/adapters/local-files/src/` — lib.rs, config.rs, index.rs, provider.rs, scanner.rs, transcoder.rs
**Interfaces:**
- Consumes: `domain`, `infra-wiring`
- Produces: `LocalFilesProvider`, `LocalIndex`, `TranscodeManager`, `LocalFilesBundle`
Read from `k-tv-backend/infra/src/local_files/`.
- [ ] **Step 1-6: Create local-files adapter**
- [ ] **Step 7: Verify and commit**
```bash
git add crates/adapters/local-files/
git commit -m "adapter-local-files: media provider, index, transcoder"
```
---
### Task 22: Adapter — event-publisher
**Files:**
- Create: `crates/adapters/event-publisher/Cargo.toml`
- Create: `crates/adapters/event-publisher/src/lib.rs`
**Interfaces:**
- Consumes: `domain` (EventPublisher, EventConsumer ports)
- Produces: `ChannelEventBus` (tokio broadcast-based)
```rust
pub struct ChannelEventBus {
tx: broadcast::Sender<DomainEvent>,
}
impl ChannelEventBus {
pub fn new(capacity: usize) -> Self { ... }
pub fn subscriber(&self) -> broadcast::Receiver<DomainEvent> { ... }
}
#[async_trait]
impl EventPublisher for ChannelEventBus {
async fn publish(&self, event: DomainEvent) -> DomainResult<()> {
let _ = self.tx.send(event);
Ok(())
}
}
```
- [ ] **Step 1-3: Create event-publisher adapter**
- [ ] **Step 4: Verify and commit**
```bash
git add crates/adapters/event-publisher/
git commit -m "adapter-event-publisher: broadcast channel bus"
```
---
### Task 23: Presentation crate
**Files:**
- Create: `crates/presentation/Cargo.toml`
- Create: `crates/presentation/src/main.rs`
- Create: `crates/presentation/src/state.rs`
- Create: `crates/presentation/src/context.rs`
- Create: `crates/presentation/src/factory.rs`
- Create: `crates/presentation/src/routes.rs`
- Create: `crates/presentation/src/errors.rs`
- Create: `crates/presentation/src/extractors.rs`
- Create: `crates/presentation/src/openapi/` — mod.rs + per-module files
- Create: `crates/presentation/src/handlers/` — auth.rs, channels.rs, schedule.rs, library.rs, admin.rs, config.rs, files.rs, iptv.rs
- Create: `crates/presentation/src/mappers/` — mod.rs + per-module files
- Create: `crates/presentation/src/background/` — mod.rs, library_sync.rs, auto_scheduler.rs, broadcast_poller.rs, webhook_consumer.rs
**Interfaces:**
- Consumes: Everything — domain, application, api-types, infra-wiring, all adapters
- Produces: Running HTTP server
Read old code from `k-tv-backend/api/src/`. This is the largest task.
Handlers follow this pattern:
```rust
async fn create_channel(
State(ctx): State<AppContext>,
CurrentUser(user): CurrentUser,
Json(req): Json<CreateChannelRequest>,
) -> Result<Json<ChannelResponse>, ApiError> {
let cmd = CreateChannelCommand { owner_id: user.id().value(), name: req.name, ... };
let channel = channels::create::execute(&ctx.channel_deps, cmd).await?;
Ok(Json(ChannelResponse::from(channel)))
}
```
Feature flags on Cargo.toml:
```toml
[features]
default = ["sqlite", "auth-jwt", "jellyfin"]
sqlite = ["adapter-sqlite", "infra-wiring/sqlite"]
postgres = ["adapter-postgres", "infra-wiring/postgres"]
auth-jwt = ["adapter-auth/jwt"]
auth-oidc = ["adapter-auth/oidc"]
jellyfin = ["adapter-jellyfin"]
local-files = ["adapter-local-files"]
```
- [ ] **Step 1: Create Cargo.toml with feature flags**
- [ ] **Step 2: Create state.rs, context.rs**
- [ ] **Step 3: Create errors.rs** — ApiError enum, IntoResponse
- [ ] **Step 4: Create extractors.rs** — CurrentUser, AdminUser, OptionalCurrentUser
- [ ] **Step 5: Create mappers/** — domain → api-types From impls
- [ ] **Step 6: Create handlers/auth.rs**
- [ ] **Step 7: Create handlers/channels.rs**
- [ ] **Step 8: Create handlers/schedule.rs**
- [ ] **Step 9: Create handlers/library.rs**
- [ ] **Step 10: Create handlers/admin.rs, config.rs, iptv.rs, files.rs**
- [ ] **Step 11: Create routes.rs** — wire all handlers
- [ ] **Step 12: Create openapi/** — utoipa merge
- [ ] **Step 13: Create background/** — library_sync, auto_scheduler, broadcast_poller, webhook_consumer
- [ ] **Step 14: Create factory.rs** — build all adapters
- [ ] **Step 15: Create main.rs** — startup wiring
- [ ] **Step 16: Verify and commit**
Run: `cargo build -p presentation`
```bash
git add crates/presentation/
git commit -m "presentation: HTTP server with handlers, routes, OpenAPI, background tasks"
```
---
### Task 24: MCP crate
**Files:**
- Create: `crates/mcp/Cargo.toml`
- Create: `crates/mcp/src/main.rs`
- Create: `crates/mcp/src/server.rs`
- Create: `crates/mcp/src/error.rs`
- Create: `crates/mcp/src/tools/mod.rs`, `channels.rs`, `library.rs`, `schedule.rs`
**Interfaces:**
- Consumes: `domain`, `application`, `infra-wiring`, adapter crates
- Produces: MCP stdio binary
Read from `k-tv-backend/mcp/src/`. Adapt tool implementations to call application use cases instead of domain services directly.
- [ ] **Step 1-5: Create MCP crate**
- [ ] **Step 6: Verify and commit**
Run: `cargo build -p mcp`
```bash
git add crates/mcp/
git commit -m "mcp: MCP server calling application use cases"
```
---
### Task 25: Cleanup — deprecate old code, update Docker, CI
**Files:**
- Modify: `k-tv-backend/README.md` — add DEPRECATED notice
- Modify: `compose.yml` — point to new presentation binary
- Modify: `compose.traefik.yml` — update if needed
- Modify: `.dockerignore` if exists
- Create or modify: `Dockerfile` for new structure
- [ ] **Step 1: Add DEPRECATED to k-tv-backend README**
- [ ] **Step 2: Update Docker build to use crates/presentation**
- [ ] **Step 3: Update compose files**
- [ ] **Step 4: Final full build and test**
Run: `cargo build --release -p presentation`
Run: `cargo test --workspace`
```bash
git add .
git commit -m "deprecate k-tv-backend, update Docker for new crate structure"
```