add SPA config UI, wire media/rss adapters, event-driven layout push

- React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder,
  presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000
- wire media + rss adapters into polling loop, remove xtb source type
- media adapter: read username/password from headers, proper subsonic auth
- event handler: subscribe to LayoutChanged, push screen update to clients
- fix clippy warnings across workspace (Default impls, collapsible ifs,
  redundant closures, is_none_or, unused imports)
This commit is contained in:
2026-06-19 00:12:42 +02:00
parent 21c08911df
commit 26ebfad3a2
175 changed files with 12338 additions and 801 deletions

147
docs/k-frame-spa-handoff.md Normal file
View File

@@ -0,0 +1,147 @@
# K-Frame SPA Handoff
## What is K-Frame
IoT dashboard system. Server aggregates data from configurable sources and pushes to ESP32 display clients via TCP. The server is fully functional — SQLite config, REST API, TCP broadcasting, data source polling. ESP32 firmware works end-to-end (display renders widgets). Now needs a config UI.
## What this session should build
A React SPA (config/admin UI) for the K-Frame server. The SPA is at `/mnt/drive/dev/k-frame/spa/` — fresh Vite + React 19 + shadcn/ui + TanStack Router + TanStack Query setup. Currently shows a placeholder page.
## Existing artifacts to read first
- **Design spec**: `docs/superpowers/specs/2026-06-18-k-frame-design.md`
- **Domain glossary**: `CONTEXT.md`
- **ADRs**: `docs/adr/0001-event-driven-cqrs.md`, `0002-static-dispatch-over-trait-objects.md`, `0003-postcard-over-flatbuffers.md`
- **API types (DTO definitions)**: `crates/api-types/src/` — widget.rs, data_source.rs, layout.rs, preset.rs. These define the exact JSON shapes the API accepts/returns.
## REST API (server runs on :3000)
All endpoints return/accept JSON.
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/widgets | List all widgets |
| POST | /api/widgets | Create widget |
| GET | /api/widgets/{id} | Get widget |
| PUT | /api/widgets/{id} | Update widget |
| DELETE | /api/widgets/{id} | Delete widget |
| GET | /api/data-sources | List all data sources |
| POST | /api/data-sources | Create data source |
| GET | /api/data-sources/{id} | Get data source |
| PUT | /api/data-sources/{id} | Update data source |
| DELETE | /api/data-sources/{id} | Delete data source |
| GET | /api/layout | Get current layout (nullable) |
| PUT | /api/layout | Update layout |
| GET | /api/presets | List presets |
| POST | /api/presets | Create preset |
| GET | /api/presets/{id} | Get preset |
| DELETE | /api/presets/{id} | Delete preset |
| POST | /api/presets/{id}/load | Load preset as active layout |
### Key JSON shapes
**Widget** (create/update):
```json
{
"id": 1,
"name": "weather",
"display_hint": "icon_value",
"data_source_id": 10,
"mappings": [
{"source_path": "$.main.temp", "target_key": "temperature"},
{"source_path": "$.weather[0].icon", "target_key": "icon"}
],
"max_data_size": 2048
}
```
`display_hint` values: `"icon_value"`, `"text_block"`, `"key_value"`
**Data Source** (create/update):
```json
{
"id": 10,
"name": "weather",
"source_type": "weather",
"poll_interval_secs": 300,
"url": "https://api.openweathermap.org/...",
"api_key": "xxx",
"headers": []
}
```
`source_type` values: `"weather"`, `"media"`, `"rss"`, `"http_json"`, `"webhook"`
**Layout**:
```json
{
"root": {
"type": "container",
"direction": "row",
"gap": 4,
"padding": 2,
"children": [
{
"sizing": {"type": "flex", "value": 1},
"node": {"type": "leaf", "widget_id": 1}
},
{
"sizing": {"type": "fixed", "value": 80},
"node": {"type": "leaf", "widget_id": 2}
}
]
}
}
```
Nodes are recursive — containers can nest.
**Preset**:
```json
{"id": 1, "name": "dashboard", "layout": { "root": { ... } }}
```
## Pages to build
1. **Dashboard** — overview of connected clients, active data sources, current layout. Landing page.
2. **Data Sources** — CRUD list. Form: name, source_type (select), URL, API key, poll interval, headers.
3. **Widgets** — CRUD list. Form: name, display_hint (select), data_source_id (select from existing sources), key mappings (dynamic list of source_path + target_key pairs), max_data_size.
4. **Layout Builder** — visual tree editor. Add containers (row/column), nest them, place widgets as leaves, set sizing (fixed/flex), gap, padding. This is the most complex page.
5. **Presets** — save current layout as preset, load presets, delete presets.
## SPA tech stack (already set up)
- React 19 + TypeScript
- Vite 8
- shadcn/ui (components already installed in `src/components/ui/`)
- TanStack Router (not yet configured with routes)
- TanStack Query (not yet configured with provider)
- Tailwind CSS 4
- Bun (lockfile is bun.lock)
## Server integration
The SPA's built files need to be served by the Axum server. Two approaches:
1. **Dev**: SPA runs on Vite dev server (:5173), proxies API calls to :3000. Add proxy config to `vite.config.ts`.
2. **Prod**: `bun run build` outputs to `spa/dist/`, Axum serves these as static files. Need to add static file serving to the http-api adapter.
The Vite proxy setup is needed first so development works.
## User preferences
- Concise, no filler
- No mocking — test against real API
- Clean code, modules, no huge files
- shadcn components for all UI elements
- TanStack Router for routing, TanStack Query for data fetching
- No Co-Authored-By in commits
## What NOT to change
- No changes to Rust crates (domain, application, adapters, etc.)
- No changes to the ESP32 firmware
- API is stable — build against it as-is
## Suggested skills
- `superpowers:brainstorming` — for designing the layout builder UX (most complex page)
- `frontend-design` — for visual design direction, making it not look like a default template
- `shadcn` — for component usage, composition, and styling patterns