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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
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>
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>
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>
- 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>
/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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
PG migrations still had the original table structure (layouts with
template_id/display_id, layout_cells with region_name) that SQLite
dropped in v0.5. PG deploy would fail because repo code expects the
final schema.
Fixes: layouts table (removed template_id/display_id/is_default),
layout_cells (removed region_name), added display_layouts join table,
kiosks.encrypt_key_encrypted, entities.name UNIQUE, all missing
indexes (sessions active, event_log received, audit_log actor,
firmware version/arch unique), foreign keys on pairing_codes/
event_log/firmware/rollouts, kiosk_gpio_bindings.created_at +
CHECK constraints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add cloud_accounts table to PostgreSQL tenant migrations (was only
in SQLite).
- Artifact cleanup now skips releases referenced by active/queued/paused
rollouts (CASCADE would delete the rollout).
- Add invisible cursor theme install to setup-pi-kiosk.sh (was only
in pi-gen image build).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cursor: install theme as index.theme (XCursor spec) not just
cursor.theme. Add WLR_XCURSOR_THEME env var for wlroots compat.
Piwiz: broader purge (rpi-first-boot-wizard, raspi-config triggers,
profile.d scripts, firstrun.sh). Mark first-boot done via userconf
marker file.
Migration: add encrypt_key_encrypted, cloud_accounts, and ONVIF event
columns to catch-all backfill so PRAGMA user_version skips can't miss
them.
Artifact cleanup: delete yanked firmware/OS files + prune to 5 most
recent per channel. Runs every 6h. Stops disk from filling up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloud accounts page was raw HTML with no sidebar/topbar. Converted to
jsx-htmx CloudAccountsPage component matching all other admin pages.
Docker postgres bumped 16→18. npm: @types/node, tsx, ws updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>