12 Commits

Author SHA1 Message Date
itsrubberduck
c8c2365c4d refactor(split): make the app self-hosting ready
Remove the transitional website auth, admin hooks, SEO and hosted analytics together, then align the app routes, runtime configuration, tests and dependencies. These changes form one atomic cleanup because the filtered app must switch its identity and runtime surfaces as a unit.
2026-07-27 19:17:27 +02:00
itsrubberduck
2b792eb57f refactor(split): decouple app from website in preparation for the repo split
Phase 0 of the OpenSquawk repo separation. Everything happens inside the
monorepo so that the split itself becomes a mechanical path filter — filtering
first and repairing afterwards would leave two broken repos at once.

AUTH_MODE (0.1)
  New server/utils/authMode.ts, session.ts and jwt.ts (the latter extracted
  from auth.ts). requireUserSession now resolves in three steps: the app's own
  session cookie, an app-minted bearer token, then the website access token.
  Only the last one is transitional; it is marked PHASE 1 and disappears with
  the User collection. The app's session is its own JWT in a host-only cookie
  plus a short-lived bearer, so the existing Authorization call sites are
  unchanged.

  AUTH_MODE defaults to 'sso', not 'open' as the plan proposed: while the admin
  and editor surface still lives here, an unset variable would otherwise serve
  it to everyone as a local admin. requireAdmin additionally refuses in open
  mode. Both are one-line removals in Phase 1 and marked as such.

SSO handoff (0.2)
  Issuer: /api/service/auth/sso/{authorize,exchange}. Codes are stored as
  SHA-256 hashes with a TTL index and claimed by a single atomic update, so
  concurrent redemption cannot succeed twice. redirect_uri is matched against
  SSO_REDIRECT_ORIGINS by exact origin — a prefix check would accept
  app.opensquawk.de.evil.tld. There is no default and no wildcard: an empty
  allowlist disables the handoff rather than opening a redirector.
  Consumer: /api/auth/sso/callback plus app/pages/auth/callback.vue. The
  browser only ever carries the code; it is redeemed server-to-server.

Hardcoded values and leaks (0.3)
  Hotjar ID, the dome-light webhook URL and the bug-report recipient were
  compiled in. All three are env-gated and off by default now, so a foreign
  instance cannot ship analytics, cockpit telemetry or its users' bug reports
  to us. Setting HOTJAR_ID, DOME_LIGHT_WEBHOOK_URL and BUG_REPORT_NOTIFY_EMAIL
  restores the current behaviour on opensquawk.de.

Two databases, no shared Mongo (0.6)
  AppUser mirrors an identity locally. Its _id is deliberately the SSO subject,
  i.e. the website's User._id, so every existing LearnProfile, PilotProfile and
  BridgeToken reference keeps resolving without a migration.
  telemetry.ts mirrors records to the hosted service only when TELEMETRY_URL
  and SERVICE_SECRET are both set — the self-host default is that nothing ever
  leaves the instance. It writes locally first, buffers with a bound, drops on
  overflow and never blocks the request path.
  /api/service/user-deleted purges the app's half on account deletion. Unlike
  telemetry this is deliberately loud: the admin delete aborts with the user
  intact if the purge fails, because their id is the only handle for retrying.
  ?force=true overrides it and says so in the response.

Also here
  /api/service/analytics/product-session was an unauthenticated public write
  endpoint; it moves to /api/analytics/product-session behind the auth guard.
  The bridge no longer populates against User but resolves through the mirror,
  backfilling missing rows so live bridges never have to re-pair.
  .claude/worktrees was tracked and would have reached the public repo.

scripts/split-paths.txt carries the filter list, verified by
scripts/verify-split-paths.mjs: every path exists, nothing website-only is
kept, and no kept file imports a dropped one. That check found real gaps —
tests/ cannot be taken wholesale, and two shared modules were missing. Ten
remaining edges are allowlisted, each annotated PHASE 1 in the code.

Open item, flagged and not resolved: flightlabTelemetryStore is an in-process
singleton written by the bridge (app) and read by FlightLab (website). Two
repos means two processes, so that read breaks regardless of which side it
lands on. FlightLab needs an HTTP path in Phase 2/3.

Verified: 609 tests pass, vue-tsc clean. Ran against two throwaway local
MongoDBs: open mode reaches /classroom and /live-atc with no login and
persists progress; the full SSO loop works and the mirror _id matches the
website User._id; lookalike origins, code reuse, forged codes and wrong
service secrets are all rejected; ingest is idempotent on bug-report code;
deletion purges all five collections; and with the app unreachable the admin
delete fails 502 with the user still present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:46:34 +02:00
leubeem
e21414e45a feat(pm): ignore sub-threshold voice transmissions (STT noise gate)
Whisper hallucinates short real words ("Test", "Thank you", "Okay") on
near-silent or noisy audio. Unfiltered these reached the backend as a wrong
readback attempt — counting toward the 3x-skip — and triggered paid
LLM-router calls. Drop voice (PTT) transcripts below NUXT_PUBLIC_PTT_MIN_WORDS
(default 2); text input is exempt so deliberate short commands still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:35:47 +02:00
leubeem
9802c438a5 feat(pm): LLM routing endpoint, usage capture, and admin review view
Backend counterpart to the Python engine's semantic router.

- POST /api/decision/route: service-secret-guarded endpoint the Python
  backend calls on regex-miss. Calls gpt-5-mini (ROUTER_LLM_MODEL),
  validates the chosen id against the candidate set, and writes both a
  UsageEvent (central cost ledger) and a routing-review record — including
  timeouts, with timeoutMs + actual latencyMs — so the budget can be tuned
- LlmRoutingDecision model + GET /api/admin/llm-routing (paginated,
  status-filtered, per-status counts)
- admin "LLM Routing" tab: transcript vs expected phrase, candidate chips
  with the chosen one highlighted, latency/budget chip, model reason
- serviceAuth util (mirrors CRON_SECRET pattern)
- .env.example: ROUTER_LLM_MODEL, SERVICE_SECRET

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:27:57 +02:00
leubeem
309bde3fcd fix(security): mandatory cron secret + reject placeholder JWT secrets (SEC-07, OPS-02, SEC-09)
SEC-07 — committed secrets:
- Replace real-looking defaults in .env.example (JWT_SECRET/JWT_REFRESH_SECRET
  "changeme", MANUAL_INVITE_PASSWORD "pm.local@zghl.de") with CHANGE_ME
  placeholders, and drop the personal DOME_LIGHT_WEBHOOK_URL default.
- Add a Nitro startup plugin (server/plugins/validate-secrets.ts) that refuses
  to boot in production when JWT_SECRET is unset, looks like a placeholder, or
  is shorter than 32 chars (warns only in development).

OPS-02 / SEC-09 — cron endpoints:
- requireCronSecret now fails closed: when no CRON_SECRET/KPI_CRON_SECRET is
  configured the endpoint returns 503 instead of being publicly callable
  (previously it allowed the request with a warning). Both cron routes already
  call the guard. Prefer the x-cron-secret header over the loggable ?secret=
  query param; document CRON_SECRET in .env.example.

Operational note: production deployments must now set JWT_SECRET (>=32 chars)
and CRON_SECRET, or the server won't start / crons return 503.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:07:27 +02:00
itsrubberduck
36ad4677dc add the lidl dome light temporarily 2026-02-18 21:49:03 +01:00
itsrubberduck
6b96b295e1 change default settings 2026-02-18 18:05:12 +01:00
Remi
18f25998f5 Allow configuring OpenAI base URL 2025-10-16 11:15:31 +02:00
itsrubberduck
60b6e1519a update env example 2025-09-18 19:46:19 +02:00
Remi
e1d892af83 feat: add manual invitation generator 2025-09-17 20:26:33 +02:00
Remi
5ad52adc8e Update contact email to info@opensquawk.de 2025-09-17 17:21:12 +02:00
itsrubberduck
d40b7d1bdc add example env 2025-09-17 16:09:08 +02:00