Commit graph

319 commits

Author SHA1 Message Date
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
4880dc32fc
feat: onError always logs, onResponse logs status+time, fix debug
onError: always log.error regardless of status code.
onResponse: log info with response status + duration in ms.
claimPairing: debug changed to info (debug not working in BSB).
Timestamps tracked via _startMs on event context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:41:16 +02:00
Mitchell R
85f8456cf0
fix: onError uses init obs when request trace missing
If event.context.obs not set, fall back to init-level obs and
flag no request trace in error message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:34:14 +02:00
Mitchell R
925d9fd6dc
feat: pass request obs into claimPairing for traced logging
claimPairing now receives the request Observable and logs the
specific reason for pending (not_found/expired/not_consumed/
missing_key). Success logged at info level with kiosk_id.
All logs correlated via request trace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:32:28 +02:00
Mitchell R
5768e325b1
feat(kiosk): include kiosk_id in Axiom log entries after pairing
After successful claim, kiosk_id from server response is stored
globally and included in all subsequent Axiom log entries for
kiosk identification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:24:52 +02:00
Mitchell R
05853473a1
feat(kiosk): Axiom log forwarding via tracing layer
Kiosk binary now forwards all tracing logs to Axiom when
BF_AXIOM_KEY + BF_AXIOM_DATASET are set at compile time via
option_env!(). Batches up to 50 entries or flushes every 10s.
No-op when keys not baked in (local dev builds).

CI build.yml passes secrets as env vars for cargo build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:23:13 +02:00
Mitchell R
35d184a6dd
fix: use pluginCwd for static files + info log on request
Static file path now uses BSB pluginCwd instead of import.meta.dirname.
Added info log with method+path on every request via per-request trace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:20:17 +02:00
Mitchell R
57348b14ab
feat: per-request tracing via BSB createTrace
Each HTTP request gets a fresh BSB trace (not a child span of init).
onRequest creates trace, stores on event.context.obs. onError logs
with trace context. onResponse ends the trace. 4xx logged as warn,
5xx as error. H3EventContext typed with obs field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:26 +02:00
Mitchell R
0113e4e54a
feat: log 5xx errors to BSB observable via h3 onError
Both admin-http and api-http now log HTTP 500+ errors with status,
path, and error message to BSB observable (warn level). Makes
server-side errors visible in Coolify/container logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 00:55:17 +02:00
Mitchell R
e189d67faa
fix(rauc): lowercase PARTUUID + format BF_DATA + remove resize
- PARTUUID in cmdline.txt and fstab must be lowercase (initramfs
  does case-sensitive match)
- BF_DATA partition now formatted as ext4 with label (was zeroed)
- Remove resize flag from cmdline.txt (breaks GPT layout)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 00:22:58 +02:00
Mitchell R
e7cf1d1e4f
fix(rauc+kiosk): use PARTUUID not LABEL, add XDG_RUNTIME_DIR
Repartition: reverted auto_initramfs=0 (Pi 5 needs initramfs).
Now creates GPT first, reads PARTUUIDs via sfdisk -J, patches
cmdline.txt with real PARTUUID. Initramfs resolves PARTUUID
natively.

Kiosk service: added XDG_RUNTIME_DIR=/run/user/1000 — cage
needs this for Wayland socket creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:24:22 +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
649c64728a
fix(pi-gen+rauc): stop purge from removing cage + fix boot
1. Desktop purge was using wildcard lx* which removed libwlroots
   and cage as dependency. Now uses specific package names +
   apt-mark manual cage to protect it from autoremove.
2. Per-user cursor theme for bfkiosk (~/.icons/default/index.theme).
3. Repartition disables auto_initramfs in config.txt (initramfs
   cant resolve LABEL= roots). Also handles root=/dev/* format
   in cmdline.txt sed replacement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:54:21 +02:00
Mitchell R
1ff0f9928c
fix(rauc): remove BF_BOOTSEL, boot from partition 1
Pi 5 bootloader needs config.txt on partition 1. Old layout had
BF_BOOTSEL there with only autoboot.txt. Now 5 partitions:
BF_BOOT_A(1), BF_BOOT_B(2), BF_ROOT_A(3), BF_ROOT_B(4), BF_DATA(5).
autoboot.txt on each boot partition for A/B tryboot switching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 06:06:52 +02:00
Mitchell R
d7f3b12488
fix(pi-gen): create bfadmin in chroot, remove pi-gen username/password
Pi-gen username/password config triggers firstboot wizard AFTER
custom stages — reinstalls userconf and undoes our purge. Removed
those params from pi-gen-action config. Now create bfadmin user
directly in chroot script with password expiry on first login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 06:00:58 +02:00
Mitchell R
9d27fe9323
fix(kiosk): nuclear piwiz kill — purge entire desktop + force cmdline
Piwiz still appeared after package purge alone. Now:
1. Purge ALL desktop packages (lxde, labwc, wayfire, lightdm, xorg)
2. autoremove orphaned deps
3. Force systemd.unit=multi-user.target in kernel cmdline

No desktop = no piwiz. Period.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 05:22:54 +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
6ec2dcbec4
fix(docker): align BF_PG_DATABASE to BF_PG_DB
Compose postgres uses POSTGRES_DB from BF_PG_DB. Dockerfile ARG
was BF_PG_DATABASE causing mismatch. PG error 3D000 db not found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 05:06:56 +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
664e6e1548
fix(build): inline db config schema for BSB schema extractor
BSB bsb-plugin-cli build extracts schemas statically and cannot
resolve cross-file imports. Inlined the anyvali db config schema
in each plugin's ConfigSchema. Shared DbConfig type stays in
shared/db/config.ts (type-only imports work fine).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:22:49 +02:00
Mitchell R
9e7542a424
fix: regenerate lockfile for package name betterframe
Lockfile had old @betterframe/server workspace reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:19:38 +02:00
Mitchell R
8abfec1867
fix(docker): package name betterframe + copy nodered workspace pkg
Package name changed from @betterframe/server to betterframe to
match BSB plugin path. Added nodered/package.json COPY so npm ci
can resolve the workspace dependency graph.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:17:43 +02:00
Mitchell R
0bb8fb68c9
feat(cloud-cameras): add EZVIZ, Reolink, Eagle Eye providers
EZVIZ: consumer Hikvision cameras via Open Platform API (appKey/appSecret).
Reolink: local HTTP API + RTSP (no cloud API available).
Eagle Eye Networks: OAuth2 cloud VMS with HLS relay URLs.
2026-05-24 02:54:49 +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
Mitchell R
4062b8bb6f
fix(db): SQLite adapter coerce true/false to 1/0 in params
node:sqlite rejects JS booleans as bind params. SQLite adapter now
converts true→1, false→0 before binding. Mirrors the PG compat
approach from the other direction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:29:46 +02:00
Mitchell R
6df11b6c18
fix(config): strip :-default from template (envsubst incompatible)
envsubst only handles ${VAR}, not ${VAR:-default}. The :-default
syntax is bash-only. Defaults come from Dockerfile ARG declarations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:28:09 +02:00
Mitchell R
bb1893719d
fix(docker): BSB v9 tag + package:betterframe plugin resolution
- Base image: betterweb/service-base:9 (was :node which is v8)
- Plugin path: /home/bsb/node_modules/betterframe/ (flat pkg name)
- sec-config template: added package: betterframe to all 4 services
  so BSB resolves plugins from the correct npm package

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:24:08 +02:00
Mitchell R
80843367d0
fix(docker): copy plugin to /home/bsb/node_modules not /mnt/bsb-plugins
/mnt/bsb-plugins is declared as VOLUME in BSB base image. Build-time
COPY gets shadowed by empty anonymous volume at runtime. Use /home/bsb/
node_modules which BSB also searches and isn't a VOLUME.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:15:30 +02:00
Mitchell R
e355d6d0a2
fix(docker): install plugin at /mnt/bsb-plugins for BSB discovery
BSB container mode searches /mnt/bsb-plugins/node_modules/ for
plugins. Moved built output from /home/bsb to the correct external
plugin path at /mnt/bsb-plugins/node_modules/@betterframe/server/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:08:27 +02:00
Mitchell R
e5551c4591
fix(docker): set NODE_ENV=production + BSB_LIVE=true
BSB needs BSB_LIVE=true for production mode. Without it, warns about
non-production and tries to write sec-config.yaml (which is read-only).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:06:11 +02:00
Mitchell R
4b36812c80
fix(docker): remove USER directives, let BSB handle privileges
BSB entrypoint at /root/entrypoint.sh runs as root and drops
privileges itself. Our USER node blocked access to entrypoint.
Removed USER root/node, use absolute COPY paths, let BSB own
the user lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 02:03:42 +02:00
Mitchell R
3a451d88da
fix(docker): use apk not apt-get (BSB base is Alpine)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 01:59:33 +02:00
Mitchell R
88526095e2
refactor: build-time sec-config from template + Coolify build args
sec-config.yaml is now generated at Docker build time from
sec-config.template.yaml via envsubst. Secrets come from Coolify
build args (set in UI, never in git). Template uses ${VAR:-default}
placeholders — safe to commit to public repo.

- sec-config.yaml removed from git, added to .gitignore
- sec-config.template.yaml added (public, no secrets)
- Dockerfile.server: ARGs for all config, envsubst generates config
  at build time, result is chmod 444 (read-only)
- Coolify compose: removed sec-config volume mount (baked in now)
- For native installs: copy template to sec-config.yaml, fill values

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 01:51:54 +02:00
Mitchell R
aab2e928c5
reverting volume definitions 2026-05-24 01:33:07 +02:00
Mitchell R
ffe448463a
fix(config): update sec-config for Docker networking
Bind 0.0.0.0 (not 127.0.0.1) so services are reachable across
containers. Use Docker container hostnames (nodered, server, postgres)
instead of localhost. Added missing cookieName + totpIssuer to
api-http and coordinator-ws configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 00:03:45 +02:00
Mitchell R
238aa4f9af
fix: resolve merge conflict + align BSB workdir /home/bsb
Resolved coolify compose conflict — took remote bind mount pattern.
All paths now use /home/bsb (BSB container workdir, not /app).
Both compose files use bind mount for sec-config.yaml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:58:36 +02:00
Mitchell R
34331c9d0e
refactor: use BSB container + mount sec-config at runtime
Dockerfile.server now uses betterweb/service-base:node as runtime
base instead of node:24-trixie-slim + manual bsb-plugin-cli. BSB
container handles entrypoint, user, plugin loading.

sec-config.yaml removed from Docker image — must be bind-mounted
at /app/sec-config.yaml. Both compose files updated with :ro mount.
All BF_* env vars removed from compose server service.

deploy/docker/sec-config.yaml deleted (was baked in, now mounted).
version.ts path updated for new workdir /app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:56:44 +02:00
Mitchell R
eb58f1da1e
direct bind paths for docker-compose 2026-05-23 19:35:28 +02:00
Mitchell R
19f6171419
Update docker-compose.coolify.yml 2026-05-23 14:05:44 +02:00
Mitchell R
49d730cf7f
refactor: remove all process.env and envStr() from server code
All runtime config now flows exclusively through BSB plugin config
(this.config.*) or shared module parameters. No more env var overrides.

Changes:
- Delete shared/env-overrides.ts (envStr/envBool/envInt helpers)
- version.ts: remove env var chain, keep only .bf-version file + "dev"
- firmware.ts: replace BF_FIRMWARE_SIGNING_KEY env with config.signingKeyPem
  parameter, remove tryParsePrivateKey helper
- secrets.ts: replace process.env.CREDENTIALS_DIRECTORY with
  config.systemdCredsDir
- mqtt-bridge.ts: accept MqttConfig object instead of reading process.env
- service-store: replace envStr calls with this.config.*, build pgUrl from
  config fields, add pgPoolMax config
- pg-adapter.ts: accept poolMax constructor param instead of env var
- service-admin-http: add firmwareSigningKey, firmwareImportApiKey,
  otaImportApiKey, systemdCredsDir config fields; pass to shared modules
- middleware.ts: replace tokenMatchesEnv with tokenMatchesExpected using
  deps.firmwareImportApiKey/otaImportApiKey
- service-api-http: add mqttUrl/mqttUsername/mqttPassword/mqttTopicPrefix
  config fields; pass to initMqttBridge
- service-coordinator-ws: replace envStr calls with this.config.*
- sec-config.yaml: add all new config fields with sensible defaults
- docker-compose.coolify.yml: remove all BF_* env vars from server service

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 13:22:44 +02:00
Mitchell R
bab194a184
fix(db): add RETURNING id to all INSERTs for PG compat
PG doesn't populate lastInsertRowid without RETURNING clause.
SQLite 3.35+ also supports RETURNING. Added to all 14 INSERT
statements that use auto-generated integer IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 13:00:18 +02:00
Mitchell R
6e3a893421
fix(db): use native booleans instead of B() integer coercion
PG rejects integer 0/1 for BOOLEAN columns. Replaced all B() calls
with native JS booleans — works for both SQLite (coerces true→1,
false→0) and PG (native BOOLEAN). Removed B() import and PG adapter
coercion hack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 12:55:04 +02:00
Mitchell R
5cefa04a45
fix(db): PG adapter coerce 0/1 to boolean for PG strict typing
PG rejects integer values for BOOLEAN columns. B() helper returns 0/1
for SQLite compat. PG adapter now converts 0→false, 1→true in params
before sending — safe for both INTEGER and BOOLEAN column types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 12:50:07 +02:00
Mitchell R
e71189b874
fix(pg): mount at /var/lib/postgresql for PG 18+
PG 18 uses major-version-specific subdirs under /var/lib/postgresql.
Old mount at /var/lib/postgresql/data breaks pg_upgrade compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 12:23:18 +02:00
Mitchell R
1421feb7b4
fix(hikconnect): rewrite to HikCentral Connect OpenAPI v2.15
Was using consumer api.hik-connect.com (wrong API). Rewritten to use
HikCentral Connect enterprise API per vendor docs:

- Auth: POST /api/hccgw/platform/v1/token/get with appKey + secretKey
- Cameras: POST /api/hccgw/resource/v1/areas/cameras/get (paginated)
- Live view: POST /api/hccgw/video/v1/live/address/get → RTMP URL
- Credential fields: app_key (AK), secret_key (SK), region
- Region-specific server addresses (eu/us/sg/sa/ru)
- Token response returns areaDomain for subsequent calls
- RTMP protocol=3, quality=1 (HD), expireTime=86400 (24h)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 12:16:42 +02:00
Mitchell R
9006364d5e
Updated gitignore to ignore more api docs 2026-05-23 12:03:38 +02:00
Mitchell R
a9484d1dd7
feat(cloud-cameras): type=cloud + bidirectional sync + PG default
Cloud cameras are now a distinct type ('cloud') managed entirely by
sync. Bidirectional: cameras added in vendor cloud appear automatically,
removed cameras get deleted. Cloud cameras and their entities are
read-only in admin UI — no manual editing.

- Camera type CHECK widened to include 'cloud'
- New columns: cloud_account_id, cloud_vendor_camera_id,
  cloud_stream_url, cloud_stream_type
- Repo: upsertCloudCamera, deleteCloudCamerasNotIn,
  listCloudCamerasByAccount
- Sync replaces import: full reconciliation per account
- Hik-Connect: fetch HLS preview URLs via previewURLs endpoint
- Tuya: fetch stream URLs during sync (not just on demand)
- Kiosk API: GET /api/kiosk/cameras/:id/stream returns fresh
  relay URL from vendor (session-based URLs expire)
- Cloud cameras show read-only detail page with cloud badge
- Coolify compose: postgres 18 as default, BF_DB=postgres,
  server depends_on postgres healthy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 11:36:49 +02:00