Files
k-frame/docs/k-frame-spa-handoff.md
Gabriel Kaszewski 26ebfad3a2 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)
2026-06-19 00:12:42 +02:00

5.2 KiB

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):

{
  "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):

{
  "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:

{
  "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:

{"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