Fleet overview N+1:
- InstanceResourceService gains a 30s TTL cache with single-flight
coalescing; the overview no longer fires six live upstream requests per
device on every render (plus the re-login storm), while the per-device
detail route probes live via force:true
Event journal becomes live:
- job terminal transitions (manual executions and the interrupted sweep)
now append to the journal, so /api/v1/events SSE feeds the frontend's
invalidation controller that was built but never received events
- journal pruning moves off the append hot path (was an unindexable
full-table json_extract scan per insert) onto the retention timer
Console auth hardening:
- scrypt upgraded from N=16384 to N=2^16 (OWASP interactive guidance);
a new password_kdf column records the derivation per row and legacy
hashes rehash transparently on the next successful login without
invalidating sessions (migration 18)
Legacy stack:
- instance URL validation blocks IPv4-compatible IPv6 after WHATWG
canonicalization (::a9fe:a9fe metadata, ::7f00:1 loopback slipped past)
- status polls cool down auto-login for 60s after a failed attempt so a
stale saved password cannot hammer the device into an account lockout
Build hygiene:
- web bundle splits app (410kB) from vendor (212kB) so framework code
stays cacheable across releases; stale root package-lock.json removed
(pnpm is the only lockfile)
Legacy panel:
- upstream body reads now carry their own deadline and a 10 MB byte budget;
a stalled modem can no longer hang /api/status fan-out forever nor OOM
the proxy (request headers alone had the timeout, bodies had none)
- add X-Frame-Options DENY / CSP frame-ancestors none / nosniff; the panel
(delete-instance and confirmed-write dialogs) is no longer clickjackable
- only send a JSON content-type when the API console request has a body, so
payload-less dangerous writes stop failing with 400 and burning the
one-use confirmation token
- register a form-urlencoded parser (the proxy branch was unreachable) and
drop the multipart parser that buffered up to 60 MB before rejecting;
bodyLimit drops to 2 MB; framework-level 415 keeps the stable error body
- remove /api/sms/send from readable paths: GET bypassed the write
confirmation for a send endpoint
- /api/instances/:id/login maps upstream failures to a stable 502 instead
of leaking raw error text
- guard MULTI_SIMADMIN_TIMEOUT_MS parsing (NaN aborted every request);
prune dead code (buildClients, cookie expando no-op)
Control plane:
- deleting a notification channel detaches it from rules instead of leaving
dangling ids that made every referencing rule unreadable and silently
dropped future notifications; rule reads tolerate unknown ids
- startup sweep resets notification_queue rows stranded in 'sending' by a
crash (mirrors the sms outbox sweep); terminal outbox rows are pruned on
the retention timer
- /api/v1/metrics no longer emits operator-assigned node names on the
session-free scrape; login limiter map is bounded and pruned; Secure
cookie honors the gateway-declared x-forwarded-proto
- webhook delivery sets redirect: manual (signed payloads are not replayed)
- SMTP envelope sender is validated against CR/LF smuggling
- scheduled reboots with delaySeconds != 3 fail fast at the dispatcher with
a clear reason instead of burning every retry; contract narrowed to the
pinned baseline