Commit graph

31 commits

Author SHA1 Message Date
Mitchell R
e0941f533d
feat: AbleSign dropdown nav + screens/content/playlists pages
- Sidebar: NavGroup component (details/summary) for AbleSign dropdown
  with Accounts, Screens, Content, Playlists sub-items
- Global screens page (/admin/ablesign/screens) — all screens across
  accounts with Internal/External badge
- Content page — aggregates media files + web apps from all accounts
- Playlists page — shows per-screen playlist items
- Auto-sync screens on account creation
- Internal/External: Internal = created via "Create & Pair" (has
  screenToken, gets entity). External = synced from AbleSign (no token,
  no entity, management-only). Only internal screens become entities.
- Entity creation only on Create & Pair path — not on sync or assign

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 01:47:17 +02:00
Mitchell R
e1a3cd1d05
fix: ONVIF single-kiosk ownership + rate limiting
Server-side:
- Bundle gen: when camera event_source is "auto", first kiosk to fetch
  bundle claims ownership → writes "kiosk:<id>" to camera row. Other
  kiosks see assigned owner and skip ONVIF subscription.
- Kiosk deletion resets event_source back to "auto" so next kiosk
  takes over.
- repo.getActiveOnvifOwners() for future use.

Kiosk-side:
- Only subscribe when event_source is "auto" or "kiosk:<MY_ID>".
  Skips "kiosk:<other_id>", "server", "none", "disabled".
- Poll interval: 3s → 10s (cameras were getting overwhelmed)
- CreatePullPoint backoff: exponential 30s→60s→120s...→600s max
- Pull errors: exponential 15s→30s→45s...→300s, resubscribe after 5
  consecutive failures instead of immediately.
- load_kiosk_id() helper reads from cached bundle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 01:40:44 +02:00
Mitchell R
a518fe17ea
fix: move AbleSign migrations to end of array (after UUIDv7 backfill)
Some checks are pending
release / meta (push) Waiting to run
release / build (push) Blocked by required conditions
Server already ran past the indices where AbleSign tables were inserted.
Moving to end ensures they get new, unrun version numbers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 17:28:38 +02:00
Mitchell R
73dbd9b6bf
feat: managed entities (read-only) + AbleSign auto-creates entity
- Entity type: add 'ablesign' to EntityType + CellContentType
- Entity.managed boolean: true for auto-created entities (camera sync,
  cloud cams, AbleSign). UI blocks editing managed entities.
- Entity.ablesign_screen_id: links to ablesign_screens row
- ensureCameraEntity now sets managed=true
- AbleSign screen creation auto-creates managed entity with
  web_url=player.ablesign.tv and ablesign_screen_id FK
- PG migration: alter entities CHECK constraint + add columns
- Entity edit route rejects POST for managed entities

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 17:08:19 +02:00
Mitchell R
c3bdcbce4c
feat: AbleSign digital signage integration
- DB: ablesign_accounts (api_key_encrypted, workspace_id) +
  ablesign_screens (ablesign_screen_id, kiosk assignment, orientation)
- API client: shared/ablesign.ts — list/register/update/delete screens,
  playlist CRUD, headless pairing (initiate player registration →
  register via admin API key → no UI shown on kiosk)
- Admin routes: account CRUD, screen sync from AbleSign API, headless
  screen creation (Create & Pair), kiosk assignment, remote delete
- Admin UI: AbleSign nav item, accounts page (add/sync/delete),
  screens page (add/assign to kiosk/delete) with kiosk dropdown
- Follows cloud camera pattern: encrypted credentials, sync from
  vendor API, assign to kiosks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 17:03:42 +02:00
Mitchell R
b93e9484ff
fix: drop FKs before UUID backfill, re-add after
SET CONSTRAINTS ALL DEFERRED only works on DEFERRABLE constraints.
Ours aren't. Instead: save all FK definitions to jsonb array, drop
them all, do the id replacements unconstrained, re-add from saved
definitions. Same pattern as the type-conversion migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:42:35 +02:00
Mitchell R
420463afdc
feat: backfill bare-integer IDs with UUIDs in existing rows
Adds migration that finds all rows where id matches ^[0-9]+$ (legacy
integer IDs converted to text strings), generates a UUID for each,
updates all FK references dynamically via information_schema, then
updates the PK. Existing data (cameras, layouts, kiosks, etc.) gets
proper UUID IDs. New rows already use UUIDv7 from repository.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:38:55 +02:00
Mitchell R
9dc6119791
fix: also convert *_by columns (uploaded_by, created_by) to TEXT
Dynamic column detection only matched 'id' and '*_id' patterns.
firmware_releases.uploaded_by and similar FK columns use '_by' suffix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:34:27 +02:00
Mitchell R
02b69713c3
fix: dynamic FK re-add with column existence check
Previous migration hardcoded ALTER TABLE ADD CONSTRAINT for FK re-add,
but production DB may have different columns than CREATE TABLE schema
(api_keys had no user_id). Now uses _bf_add_fk() helper that checks
both source column and target column exist before adding FK. Skips
silently if either is missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:32:18 +02:00
Mitchell R
fe9c51d3f0
fix: exclude setup_state from UUIDv7 migration
setup_state is a singleton (INTEGER PK CHECK(id=1)), not an entity.
Converting its id to TEXT breaks the CHECK constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:28:53 +02:00
Mitchell R
108123fb86
fix: dynamic FK drop in UUIDv7 migration — handle all constraints
Previous migration hard-coded FK constraint names and missed
firmware_releases.uploaded_by, firmware_rollouts.created_by/release_id,
os_update_releases.uploaded_by, os_update_rollouts.created_by/release_id,
pairing_codes.consumed_by_kiosk_id, entities.camera_id.

Now uses information_schema to dynamically drop ALL FK constraints
before type conversion, and dynamically finds ALL integer id/*_id
columns to convert. No more missed FKs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:26:46 +02:00
Mitchell R
9b4032ca8a
refactor: decommission SQLite + add UUIDv7 PK migration for existing PG
- Delete sqlite-adapter.ts and migrations.ts (SQLite path removed)
- Remove driver/sqlitePath from all config schemas + sec-config template
- init.ts now PG-only, no SQLite branch
- db-adapter.ts dialect narrowed to "postgres" only
- Add in-place UUIDv7 migration: detects INTEGER PKs in existing PG
  databases, drops FK constraints, ALTER COLUMN TYPE to TEXT for all
  15 entity tables + their FK columns, re-adds FK constraints. Idempotent
  (skips if already TEXT). Existing integer IDs become string "1", "2"
  etc — new inserts use proper UUIDv7 from repository.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 13:22:29 +02:00
Mitchell R
0c74e26e42
feat: expand BF_DATA on first boot + wire update progress banner + partition reporting
- Add betterframe-expand-data systemd service: growpart + resize2fs on
  BF_DATA (last partition) so it fills the full SD card on first boot.
  Solves the "No space left on device" issue with OS update downloads.
- Change OS update staging dir from /var/tmp/betterframe to
  /var/lib/betterframe/tmp (on BF_DATA partition, not rootfs).
- Wire firmware and OS update progress callbacks into the GTK overlay
  banner — shows "OS Update v1.2.3: Downloading — 45%" etc.
- Add per-partition disk reporting in heartbeat (/, /boot/firmware,
  /var/lib/betterframe) with total/used/free/percent.
- Display partition table on kiosk detail page in admin UI.
- PG + SQLite migrations for partitions_json column on kiosks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 08:09:20 +02:00
Mitchell R
66653af360
feat: implement multi-tenant support with PG schema isolation
Adds tenant management for PostgreSQL deployments. Each tenant gets its
own PG schema (tenant_<slug>) with a full set of BetterFrame tables.
SQLite deployments stay single-tenant with no behavior change.

Key changes:
- Run PUBLIC_MIGRATIONS (tenants + global_admins tables) during PG init
- Auto-create "default" tenant (schema=public) on first boot
- createTenantSchema() runs TENANT_MIGRATIONS in a new PG schema
- DbAdapter.setSearchPath() for per-request schema switching (PG)
- Tenant CRUD in Repository (listTenants, create, update, delete)
- Middleware resolves bf_tenant cookie and sets search_path per request
- Admin UI: /admin/tenants with CRUD + tenant switching via cookie
- Tenant dropdown in topbar (Layout) when >1 tenant exists
- Tenant nav item in sidebar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 07:22:01 +02:00
Mitchell R
64f47a9a6b
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.

Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
  RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
  u32 (old) and String (new) IDs for backward compatibility

Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 07:11:45 +02:00
Mitchell R
69e51197bf
refactor(streams): store RTSP components separately for ONVIF cameras
ONVIF-discovered camera streams now store rtsp_host, rtsp_port, and
rtsp_path as separate columns instead of baking credentials into a
pre-built URL. This fixes XML entity issues (&amp;), special character
password breakage, and credential duplication across streams.

Bundle generation builds the final playable URL at delivery time using
components + camera row credentials with proper URL encoding. Existing
RTSP-type cameras with only rtsp_uri continue to work unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 06:51:33 +02:00
Mitchell R
24f9532adf
feat(events): mark subscriptions active on event receipt
When /api/kiosk/event receives an ONVIF event, call
markEventReceived(camera_id, topic) to flip subscription
status from pending → active (orange → green in admin UI).
Also added event_source/event_sink fields to subscription mapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:04:11 +02:00
Mitchell R
01d9098af2
chore: gitignore doc files + remove from tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 04:16:33 +02:00
Mitchell R
b0f42d29c2
feat: pre-boot firmware self-update + public endpoints
Kiosk checks for stable firmware update before pairing. If available,
downloads + verifies + swaps binary and restarts. No auth needed.

Server: GET /api/firmware/public/check (stable channel, no auth)
        GET /api/firmware/public/download/:id (rate-limited, no auth)

Kiosk: check_public() + apply_public() in firmware.rs. Called from
ui.rs worker thread before entering pairing loop. kiosk_app_version
made pub for access from ui.rs.

Also includes kiosk_id deserialization fix (Value instead of String).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 04:16:17 +02:00
Mitchell R
6a74b96570
chore: remove accidentally committed doc files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 02:41:01 +02:00
Mitchell R
dba06b63db
fix: require() to dynamic import() in ESM + event subscriptions
routes-os-updates.ts used require() which fails in ESM. Changed to
dynamic import(). Also includes persistent event topic subscriptions
with status tracking (inactive/pending/active/failed), merge-only
refresh, and colored status dots in camera detail UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 02:40:46 +02:00
Mitchell R
cce9b51887
feat(events): add persistent ONVIF event topic subscriptions with status tracking
Add camera_event_subscriptions table to track per-camera per-topic
subscription state (inactive/pending/active/failed). Refresh-events
handler now merges discovered topics instead of replacing, so topics
are never lost when a camera goes temporarily offline. Admin UI shows
colored status dots and last-event timestamps per topic, with a
"subscribe all inactive" button to queue subscriptions for kiosk pickup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 02:38:43 +02:00
Mitchell R
3ee79b9e83
fix(db): replace enabled = 1 with enabled = true in WHERE clauses
PG BOOLEAN columns cant compare with integer literals. Five
queries used enabled = 1 in WHERE, causing boolean = integer
operator error on kiosk auth, bundle fetch, and heartbeat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 02:08:09 +02:00
Mitchell R
edf3c2e2eb
fix(db): handle PG JSONB as native objects in j() mapper
PG returns JSONB columns as native JS objects, not strings.
j() helper only handled strings via JSON.parse, returning the
fallback for objects. Now passes through objects/arrays directly.
Fixes pairing extras (kiosk_key_plaintext), capabilities, scopes,
and all other JSONB fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:58:06 +02:00
Mitchell R
f4be3ee901
fix(db): handle PG Date objects in string mappers
PG driver returns TIMESTAMPTZ as JS Date objects. The s() and sn()
mapper helpers only checked for typeof string, returning null/empty
for Date objects. This broke consumed_at check in pairing (always
null), expires_at comparisons (Invalid Date), and all other
timestamp fields.

Now: Date instances are converted to ISO strings via toISOString().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:54:08 +02:00
Mitchell R
c91f9cb450
feat(obs): add observability tracing throughout server
Repository _run/_get/_all now create child spans with db.statement
when an Observable is set via withObs(). Bundle generation and pairing
confirmation accept optional obs for span-based tracing. Key admin
route handlers (camera/layout/kiosk CRUD, cloud sync) log structured
info lines with actor and resource id. Kiosk API routes (heartbeat,
bundle, event, firmware check, OS check) log kiosk_id on entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:47:24 +02:00
Mitchell R
e3254ed46b
fix(db): rewrite INSERT OR IGNORE to ON CONFLICT DO NOTHING for PG
SQLite INSERT OR IGNORE syntax not valid in PG. PG adapter now
auto-rewrites to INSERT INTO ... ON CONFLICT DO NOTHING. Fixes
attach layout, label assignments, and join table inserts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 00:12:25 +02:00
Mitchell R
41e9991891
fix(db): replace remaining hardcoded 0/1 with boolean params
setup_state.is_complete, cluster_key_provisioned, display.is_primary,
event_log.forwarded_to_nodered all had literal 0/1 in SQL strings.
PG rejects integer for BOOLEAN columns. Changed to ? params with
true/false values — SQLite adapter coerces to 1/0 automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 05:18:43 +02:00
Mitchell R
2e88d891e1
fix(db): clean config field names under db: object
Removed redundant pg prefix — fields already nested under db:.
pgHost→host, pgPort→port, pgDatabase→database, pgUser→user,
pgPassword→password, pgPoolMax→poolMax, pgUrl→url.

Updated all 3 plugin schemas, shared DbConfig type, init.ts,
and sec-config template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 05:12:53 +02:00
Mitchell R
5c367203c6
fix(db): log failing PG migration index + SQL on error
Was crashing silently during migration. Now logs which migration
index failed and the first 200 chars of the SQL before rethrowing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 05:05:31 +02:00
Mitchell R
0479cb7b4b
refactor(db): move service-store from BSB plugin to shared/db library
Each service plugin now independently initializes its own DB connection
via shared/db/init.ts instead of depending on a central service-store
plugin. This removes the inter-plugin dependency ordering and the
plugin-registry singleton, making each service self-contained.

- Move db-adapter, repository, mappers, migrations, adapters to shared/db/
- Create shared/db/config.ts (reusable dbConfigSchema) and init.ts
- Delete service-store plugin and plugin-registry
- Add db config block to each service's ConfigSchema + sec-config template
- Move event_log purge timer into service-admin-http
- Update all import paths across shared modules and plugins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:48:32 +02:00