Panels become devices: per-device documents in the flow repo, a Devices tab

- flow/devices.py: DeviceDef in FLOWS_DIR/_devices/<id>.json, committed under
  the flow store's write lock, 409 on a stale version; nonces in DEVICES_FILE
  outside git; in-memory presence announced as device_status
- api/routes/devices.py: CRUD, pairing with an optional {name, board,
  firmware}, a pending list adopted without a code from the LAN only, unpair;
  /panels/pair, /panels/pair/{code} and GET /panels/{id} kept as aliases,
  PUT /panels/ gone
- _device_may replaces _panel_may, JWT claims unchanged
- panels.json moves into devices once at startup, then .migrated
- MCP list_devices, get_device; search category device
- Devices tab (list, detail with presence, pairing, ordered dashboards,
  touch, I/O), PanelsDialog removed, serialised optimistic saves
This commit is contained in:
Melvin Strobl
2026-10-16 15:48:36 +02:00
parent 3c0912a3fe
commit 28a565e501
56 changed files with 3539 additions and 2404 deletions
+15 -10
View File
@@ -89,7 +89,7 @@ for (const theme of ["light", "dark"]) {
await captureFlows(page, dir)
await captureDashboards(page, dir)
await capturePanels(page, dir)
await captureDevices(page, dir)
await captureMedia(page, dir)
await captureRound(page, dir)
await captureRuns(page, dir)
@@ -97,7 +97,7 @@ for (const theme of ["light", "dark"]) {
await context.close()
console.log(
` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel,app-panels,app-round,app-runs,app-run,app-run-context,app-workers}.png`,
` wrote ${dir}/{website-hero,app-login,app-dashboard,app-flows,app-flow-panel,app-panel,app-devices,app-device,app-round,app-runs,app-run,app-run-context,app-workers}.png`,
)
}
}
@@ -203,15 +203,20 @@ async function captureDashboards(page, dir) {
await page.screenshot({ path: `${dir}/app-panel.png` })
}
/** The panels dialog, scrolled to the first panel's I/O when there is one. */
async function capturePanels(page, dir) {
await page.goto(`${APP_URL}/dashboards?panels=true`, {
waitUntil: "networkidle",
})
const io = page.locator("[data-testid^=add-port-]").first()
if (await io.count()) await io.scrollIntoViewIfNeeded()
/** The Devices tab, and one device: the Demo's when it is seeded. */
async function captureDevices(page, dir) {
await page.goto(`${APP_URL}/devices`, { waitUntil: "networkidle" })
await page.waitForTimeout(1000)
await page.screenshot({ path: `${dir}/app-panels.png` })
await page.screenshot({ path: `${dir}/app-devices.png` })
const demo = page.locator("a[href$='/devices/demo']")
const card = (await demo.count()) ? demo : page.getByTestId("device-card")
if (!(await card.count())) return
await card.first().click()
await page.getByTestId("device-presence").waitFor({ timeout: 15000 })
await page.waitForLoadState("networkidle")
await page.waitForTimeout(1000)
await page.screenshot({ path: `${dir}/app-device.png`, fullPage: true })
}
/**