Skip to content

Release v0.6 — stable installable PWA, FOSS (Apache 2.0), website + docs overhaul#603

Merged
SheetMetalConnect merged 92 commits into
mainfrom
v0.6
May 25, 2026
Merged

Release v0.6 — stable installable PWA, FOSS (Apache 2.0), website + docs overhaul#603
SheetMetalConnect merged 92 commits into
mainfrom
v0.6

Conversation

@SheetMetalConnect
Copy link
Copy Markdown
Owner

Release v0.6 — stable installable PWA, FOSS (Apache 2.0), website + docs overhaul

Consolidates all v0.6 work from the feature branches into one release branch.
75 commits ahead of main.

Highlights

Licensing / positioning

  • Relicensed to Apache 2.0 — Eryxon Flow is now FOSS.
  • Truthful release history correction + honest FOSS positioning across docs.
  • Roadmap surfaced via Canny.

App / core features (consolidated)

  • ERY-34 — onboarding state hydration + tenant completion stamp.
  • ERY-46 / ERY-51 — request-correlated edge logging + activity_log persistence (observability baseline).
  • ERY-69 — mobile operator gate + launch routes.
  • ERY-71 — hardened Android packaging + self-host distribution path.
  • ERY-82 — truthful hosted / PWA scanner fallback.
  • ERY-43 / ERY-93 — pilot alert delivery + routing engine, Vault-sourced cron secret.
  • ERY-20 — supportability baseline + admin/security audit migration.
  • MCP tool handlers realigned with the DB schema.

PWA

  • Stable, installable PWA (service-worker gating, install support, auth-callback fix).

Website + docs

  • ERY-56 / ERY-60 — Astro redesign foundation, design-system conformance, buyer-path CTAs.
  • ERY-117 / ERY-124 — docs locale parity (NL / DE) + light-first defaults.
  • ERY-79 — recurring article surface for biweekly content.
  • Canonical Eryxon Design System; Starlight docs-chrome redesigned onto it.
  • Real brand mark / wordmark / favicon ported from the design kit.
  • /pricing page with the product's inline pricing + managed-rollout copy (EN / NL / DE).
  • Restored Sharp image service (the noop service broke <img> under astro dev).

CI / release

  • ERY-92 — train-vs-hotfix declaration enforced in the release workflow.
  • ERY-48 — automated Flow restore drill + recovered-env smoke checks.
  • ERY-101 / ERY-107 — fresh-tenant seed provisioning drill.

Not in this release (roadmap / WIP)

  • Native iOS + Android apps are work-in-progress and stay on the roadmap. The PWA is the stable installable surface for v0.6. Native packaging/CI lanes (ERY-99, ERY-108) continue on their own branches.

Notes for merge

  • main currently has 2 commits not on v0.6 (the ERY-107 re-drill workflow and an ERY-79 article-surface variant). These produce add/add conflicts in:
    • .github/workflows/seed-provisioning-drill.yml
    • website/src/content/docs/articles/_template.md
    • website/src/content/docs/articles/index.md
      These need a manual resolve at merge time — the underlying work exists on both sides.

🤖 Generated with Claude Code

SheetMetalConnect and others added 30 commits May 7, 2026 10:00
Systematic audit of every MCP tool against the live Supabase schema.
Fixes 10 broken tools, bringing pass rate from 16/26 to 25/26.

Schema mismatches fixed:
- jobs.customer_name -> jobs.customer (column renamed)
- jobs: remove non-existent started_at/paused_at/completed_at/resumed_at/priority columns
- parts: fix status enum pending->not_started, customer_name->customer in join
- parts: current_stage_id -> current_cell_id
- operations: remove non-existent started_at/paused_at timestamp fields
- operations: fix status enum paused->on_hold, cancelled removed
- issues: fix status enum open/in_progress/resolved -> pending/approved/rejected
- issues: fix ncr_category enum to match DB (material_defect/dimensional/surface_finish/process_error/other)
- issues: fix ncr_disposition enum (remove repair, not in DB)
- issues: remove ncr_number column reference (doesn't exist)
- issues: fix ambiguous profiles join -> use operations join instead
- substeps: description->name, completed->status (text), remove get_next_substep_sequence RPC
- cells: enforce_limit->enforce_wip_limit, show_warning->show_capacity_warning
- dashboard: replace tasks table query with operations (no tasks table)
- dashboard: fix get_production_metrics to use operation_quantities instead of non-existent operation columns
- scrap: fix material join (parts.materials.name -> parts.material)
- scrap_reasons: add includeDeleted flag (no deleted_at column)

Error handling improvements:
- wrapError now handles Supabase PostgrestError objects (plain objects with .message)
- All raw `throw error` replaced with `throw databaseError(...)` for proper error messages
- create_job: add tenant_id from TENANT_ID env var (NOT NULL constraint)

Removed:
- tasks module (no backing table in current schema)

Tests: 90/90 pass, build clean, 25/26 live stdio tools verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refresh the docs site for the v0.5 line and announce the native Android
and iOS apps that are now in development.

- Landing pages (EN/DE/NL): SaaS-style platform availability strip
  ("Native in Browser", Android Coming Soon, iOS Coming Soon), updated
  hero tagline, dedicated Native Mobile Apps section with feature
  bullets and "In Development" badges, refined Current Status cards
  pointing at v0.5.x stable + native apps coming next.
- 22 -> 24 REST endpoints across EN/DE/NL index pages and ERP
  integration page; mention pluggable planning adapters (FrePPLe,
  Odoo) alongside MCP server, MQTT, webhooks.
- Scheduling feature page: new "Planning Adapters (v0.5)" section
  covering FrePPLe (production-ready) and Odoo MRP (scaffold), the
  shared adapter interface, and the createPlanningAdapter factory.
- ERP integration page: new "Planning Adapters" integration option
  table.
- MQTT/Webhooks page: new "Reliability (v0.5)" section covering
  exponential backoff retry, per-attempt timeout, circuit breaker,
  dead letter logging, and injectable transport.
- Introduction pages (EN/DE/NL): wire up v0.5 talking points
  (24 endpoints, MCP stdio + Streamable HTTP, planning adapters,
  MQTT/webhook hardening).
- Changelog + roadmap: soften "development on hold" messaging now
  that native apps are in development; roadmap gains an
  "In Development" section for the Android and iOS apps with notes
  on offline support, fast cold start, and shared backend/API.
Adds vite-plugin-pwa + workbox so Eryxon Flow installs as a standalone
desktop app on macOS (Safari 17+ "Add to Dock" or Chrome/Edge install)
with its own Launchpad/Dock tile and icon, alongside iOS/Android home
screen and Windows Start menu support.

- Web manifest (id, scope, start_url, theme/background colors,
  display: standalone) with full PNG icon set + maskable variant
  generated from public/pwa-icon.svg via @vite-pwa/assets-generator
- Service worker (autoUpdate) precaches the SPA shell, NetworkFirst
  on /env.js so self-hosted runtime config is never stale, CacheFirst
  on fonts
- Apple/PWA meta tags in index.html; CSP gains manifest-src 'self'
- SW/manifest/env.js cache headers wired in nginx.conf, vercel.json
  and public/_headers so installs always pick up new versions
- npm run pwa:assets regenerates icons from the SVG source
Wraps the operator UI as a Capacitor WebView app (iPhone + iPad) and adds
a touch-first `/m/*` shell that the web bundle also serves as a Safari PWA.
Same React 18 bundle, same data layer — just a new chrome optimized for
the shop floor.

What's new
- Capacitor 6 config + iOS plugins (haptics, status bar, splash, keyboard,
  push, ML Kit barcode scanner, native biometric).
- `src/lib/native/*` — Capacitor wrappers that no-op gracefully on the web.
- `src/components/mobile/*` — adaptive shell: bottom tab bar on iPhone,
  split-view master/detail on iPad, plus iOS-feel `PullToRefresh`,
  `SwipeRow`, `MobileTopBar`, `SafeArea`.
- `src/pages/mobile/*` — operator-focused screens: work queue, op detail
  with bottom-anchored Start/Stop/Complete, QR/barcode scanner, activity
  timeline, issues feed, terminal overview, PIN + Face/Touch ID login.
- `/m/*` route group with platform-aware redirect from `/`.
- `mobile-ios.css` — safe areas, momentum scroll, 16px touch input rule,
  coarse-pointer hover suppression, dynamic-viewport sizing.
- iOS PWA meta tags, web manifest with shortcuts, theme-color per scheme.
- `scripts/ios-init.sh` + npm scripts (`ios:init/sync/open/run`) +
  `docs/IOS.md` covering bootstrap, permissions, push, TestFlight.
…r UX

Wraps the React SPA in a Capacitor 7 native shell so Eryxon Flow runs as a
real Android APK / AAB on phones and especially tablets (Pixel Tablet,
Galaxy Tab S9, Lenovo Tab P12, foldables). The web build stays the source
of truth — there is no parallel native tree.

Native bridge (src/native/)
- Thin abstractions over Capacitor with web fallbacks:
  haptics, scanner (ML Kit barcode + BarcodeDetector fallback), camera,
  biometric, network status, app shell (status bar, splash, hardware back).
- Platform helpers: isNativeApp / isAndroidNative / isTabletViewport.
- Components import from @/native only — never @capacitor/* directly.

Mobile shell (src/components/mobile/)
- MobileShell wraps every authenticated route: hardware back wired into
  React Router, persistent OfflineBanner, floating ScanFab on operator
  routes, body classes (app-mobile / app-android) for CSS scoping.
- ScanDialog: native ML Kit on Android, in-dialog camera preview on web,
  manual entry fallback. Haptic success/error confirms.
- ScanFab navigates with ?q=<code>; WorkQueue picks it up and filters.

Tablet-first responsive layer (src/styles/mobile.css)
- Safe-area utilities for status bar / nav bar / display cutouts.
- Touch targets bumped to ≥44 px on the mobile shell only — desktop SPA
  stays dense.
- sw600dp / sw720dp landscape breakpoints for Pixel Tablet / Galaxy Tab.
- prefers-reduced-motion honoured (Android system setting).

PWA / offline
- manifest.webmanifest with shortcuts (Work Queue, Scan, Activity) +
  maskable icons.
- sw.js: network-first for app shell, cache-first for assets, bypass for
  Supabase. Operators stay functional if shop floor WiFi blips.

Android project (android/)
- AndroidManifest: resizeableActivity, adjustResize, multi-window,
  Camera/Biometric/Push/Notifications permissions, deep links
  (eryxon:// + https://app.eryxon.eu), camera optional so kiosk tablets
  without rear cameras still install.
- Material themes with edge-to-edge dark status/nav bars,
  values-night colors, network_security_config restricting cleartext to
  localhost / LAN / .local (self-host friendly).
- Adaptive launcher icon with Eryxon brand mark + monochrome variant
  for Android 13 themed icons.
- ProGuard rules for Capacitor + ML Kit + biometric reflection paths.
- App-bundle splits (language/density/abi), Java 17, R8 release shrinking.
- Gradle daemon tuned for ML Kit's memory footprint.

Scripts + docs
- npm run android:sync / android:run / android:livereload /
  android:assemble:debug / android:assemble:release / android:bundle:release.
- docs/ANDROID.md covers prerequisites, workflows, plugin map,
  signing, FCM push setup, and self-hosted server URL via
  CAPACITOR_SERVER_URL.

Validation
- npm run build: clean.
- npm run test:run: 758 tests pass across 54 files.
- npm run lint: net delta vs baseline is -1 error / -1 warning.
- npx tsc --noEmit: no type errors in any new file.

https://claude.ai/code/session_01JMb7RSgCE6PG3inJx14LQW
… MCP is Live

The previous v0.5 refresh overstated the status of several surfaces.
Corrected across landing pages, intro pages, feature pages, changelog,
and roadmap.

Status corrections:
- Web app: Beta (was implied "Live today")
- REST API: Beta (was implied live)
- Webhooks: Beta (was implied live)
- MQTT: Beta (was implied live)
- FrePPLe planning adapter: Beta (was "production-ready")
- Odoo planning adapter: Beta (was "scaffold")
- MCP server: Live (correct, kept)
- Android (native, offline, fast cold start): Coming Soon (kept)
- iOS: Coming Soon (kept)

Visual updates:
- Hero pill strip: "Web App — Beta" (amber) instead of "Native in
  Browser" (primary blue)
- Platform 4-card row: web app gets a Beta label, integration card now
  splits "MCP server: Live" from "REST/Webhooks/MQTT: Beta"
- Mobile app section badges relabeled "Coming Soon" (was
  "In Development") to match the user-facing terminology
- Current Status cards: added Beta callouts on web app and hosted
  version cards

Content updates:
- Scheduling and ERP integration pages: adapter status table now
  reads "Beta" for both FrePPLe and Odoo, with a note that interfaces
  may still change and to pilot on non-critical work first
- Introduction pages (EN/DE/NL): new status callout pinning Beta vs
  Live vs Coming Soon for each surface, near the top
- Changelog Current Status: explicit Beta-vs-Live breakdown
- Roadmap: same Beta-vs-Live breakdown in the May 2026 status note
The MQTT reliability section suggested filtering mqtt_logs by tenant_id
and status='failed', but that table has mqtt_publisher_id and a boolean
success column instead. Tenant scope lives on mqtt_publishers, so
self-hosters need to subquery publishers by tenant first.

Replace the prose with an explicit SQL example using the real columns.

Found by chatgpt-codex-connector review on PR #580.
- MobileLogin: route to ROUTES.MOBILE.QUEUE after a successful PIN so the
  operator stays inside the touch shell (bottom tabs + scanner). Going to
  the desktop /operator/work-queue dropped them out of the mobile chrome.
- MobileIssues: align with the real `public.issue_status` enum
  (pending/approved/rejected/closed). The previous mapping
  (open/acknowledged/resolved/rejected) meant freshly reported issues
  default to `pending` and never showed up in the Open tab. The Open tab
  now bucket-tests `status === 'pending'`; everything else lands in
  Resolved.
Self-audit of the v0.5 doc refresh, addressing every nit found:

Accuracy:
- Hero description and tagline (EN/DE/NL): no longer claim native
  Android/iOS apps are available "today" — they are coming soon, only
  the browser app exists today
- Introduction Integration-First section (EN/DE/NL): retry/circuit
  breaker/dead-letter logging are MQTT-client features only, not
  webhooks; webhooks have HMAC signatures but no retry hardening.
  Re-scoped the sentence and added Beta/Live tags inline (REST Beta,
  webhooks Beta, MQTT Beta, MCP Live)
- Scheduling page: corrected FrePPLe adapter test count (21, was 20)
  and added Odoo's actual count (2) to underscore the narrower coverage

Historical entries:
- Changelog v0.5.1 entry: dropped "final active-development release"
  framing on v0.5.0 and v0.5.1 since native mobile development
  started after v0.5.1; reframed v0.5.0 as the last web-app feature
  release before the native push

Localization polish:
- DE: "gleiche Backend" -> "gleiches Backend" (Backend is neuter)
- NL: smoother phrasing for offline support claim
  ("operators tijd kunnen blijven registreren" instead of "operators
  kunnen blijven tijd schrijven")
CodeRabbit findings on PR #577:

- nginx: server-level security headers were silently dropped on locations
  that defined add_header (per ngx_http_headers_module's no-merge rule).
  Extracted X-Frame-Options / X-Content-Type-Options / X-XSS-Protection /
  Referrer-Policy into nginx-security-headers.conf and re-included from
  every location that defines its own add_header.
- vite-plugin-pwa: dropped skipWaiting + clientsClaim and switched
  registerType to 'prompt'. A new SW now installs and waits; a Sonner
  toast (PwaUpdatePrompt) shows the operator a Reload action that posts
  SKIP_WAITING to the SW. No more forced mid-shift reloads. New i18n
  keys in en/nl/de common.json.
- index.html: meta CSP shipped to production stripped of dev-only
  origins (localhost / 127.0.0.1, http+ws). Done at build time via a
  small Vite HTML transform so `vite dev` keeps HMR working.
- vercel.json: dropped Google Fonts origins (we bundle @fontsource/inter)
  so the response-header CSP matches the meta CSP and browsers no longer
  enforce a confusing intersection.

Also: registerSW.js is no longer emitted (React hook handles SW
registration directly), so the cache rules for /registerSW.js were
removed from nginx, vercel and _headers.
The Vercel deploy was failing on `npm ci` because @capgo/capacitor-native-biometric@^7 needs Capacitor 7+ as a peer, while we ship on Capacitor 6. Drop to ^6.0.4 (last v6-compatible release) and regenerate package-lock.json.

While I was in there, address the real lint findings in the mobile code:
- PullToRefresh: replace `armed.current` with `dragging` state so transition lookups don't read a ref during render.
- MobileScanner: hoist launchScan above the auto-launch effect and wrap with useCallback so eslint's no-use-before-define doesn't fire.
- MobileQueue / Activity / Issues / Login / OperationDetail / Terminal: defer the initial fetch via queueMicrotask so synchronous setState no longer runs inside useEffect; also drop the redundant setLoading(true) calls (state already starts as true).
Resolved conflicts:
- index.html: kept iOS viewport-fit=cover + maximum-scale=1, kept the PWA
  icon set + apple-touch-icon from #577. Manifest is now generated by
  vite-plugin-pwa, so the explicit <link rel="manifest"> is no longer
  needed (vite-pwa injects it). manifest-src 'self' added to CSP.
- public/manifest.webmanifest: deleted my hand-written file — the PWA
  plugin now generates it from vite.config.ts. Folded my mobile-only
  shortcuts (Queue / Scan / Issues) into the plugin's manifest config.
- package*.json: took the merged set; ran npm install to resolve.
Resolved conflicts:

- Capacitor 6 → 7 across the board. iOS plugins are bumped to v7 so they
  share the same peer (@capacitor/core@^7.6.4) as the Android plugins
  introduced by #579. Bumps @capgo/capacitor-native-biometric back to ^7.6
  too (its peer requires Cap 7).
- capacitor.config.ts: combined into a single file with both `ios:` and
  `android:` sections, plus the CAPACITOR_SERVER_URL override path from
  #579 for live-reload.
- src/lib/native → src/native: collapsed to one bridge module. Status-bar /
  keyboard helpers (iOS) and platform/scanner/biometric/network/camera/
  appShell helpers (Android) all live under src/native now. Components
  import from `@/native`; nothing else imports @capacitor/* directly.
- src/native/platform.ts: unified to detect iOS/iPadOS/Android/web with
  cached fingerprints and exposes shouldUseMobileShell / hasFinePointer.
- src/native/biometric.ts: kept the Android verifyBiometric/isBiometric
  API and added back getBiometricAvailability / verifyIdentity for the
  iOS login screen.
- src/native/scanner.ts: re-introduced ScannerUnavailableError and
  ScannerPermissionError so the iOS scanner page can route the user to
  the manual lookup fallback when the bridge isn't available.
- src/components/mobile/MobileShell.tsx: merged shells. Bottom tab bar +
  iPad split (mine) plus hardware-back wiring, OfflineBanner, and the
  app-mobile / app-android body classes (theirs).
- src/main.tsx: kept the data-platform tagging (drives mobile-ios.css)
  and added initNativeShell() to boot status bar / splash / hardware back.
- src/index.css: imports both styles/mobile.css (Android tablet) and
  styles/mobile-ios.css (iOS) — they don't overlap.
- index.html: viewport keeps user-scalable=no for the native shell while
  the new manifest meta is satisfied by vite-plugin-pwa.
- .gitignore: dropped duplicate dev-dist marker; kept the Android +
  iOS native-build ignores.
- Hooks: useHaptics now talks to the unified `haptics` API; useNative
  exposes both isNativeIOS and isAndroidNative for routing decisions.

Validation:
- npm run build → clean (PWA SW + 205 precached entries)
- npm run test:run → 54 files, 758 tests pass
Six real findings, all from the umbrella merge:

1. App.tsx (P1): drop the App-level <MobileShell> wrap. With the merge it
   was mounting around AppRoutes, so /admin and /auth routes were getting
   the bottom tab bar + offline banner + app-mobile body class, and /m
   routes ended up with a duplicate MobileShell. Mobile chrome now only
   mounts under MobileRoutes (/m/*).
2. capacitor.config.ts (P1): drop `iosScheme: 'https'` for the bundled
   case — Capacitor docs explicitly forbid `http`/`https` for the local
   WebView scheme (WKWebView reserves them) and the iOS app would fail to
   load the bundled /m/* app at startup. Falls back to the default
   `capacitor://` scheme; the remote-URL branch keeps using the matching
   protocol.
3. MobileActivity.tsx (P2): "Today" tile was reading `grouped[0]` which is
   simply the most recent day with activity, so on the first sign-in of
   the day operators saw yesterday's hours. Switched to an explicit
   today-key lookup.
4. MobileTerminal.tsx (P2): mirror the desktop terminal — load `cells`
   separately and seed the per-cell counters with the full active set, so
   idle cells render with `0 active / 0 queued` instead of disappearing
   into the "No cells configured" empty state. Also restored cell-sequence
   sort to match the desktop ordering.
5. ScanDialog.tsx + native/scanner.ts (P2): plumb an AbortSignal through
   `scanOnce` so the web-fallback `getUserMedia` stream is torn down the
   moment the dialog closes — previously the camera indicator stayed on
   until the 30s scan timeout fired.
6. vite.config.ts (P1, from #577): the prod CSP-strip plugin would also
   strip `localhost` / `127.0.0.1` from the meta CSP for self-host builds,
   which breaks the documented Docker setup that uses
   VITE_SUPABASE_URL=http://127.0.0.1:54321. Skip the strip when the
   configured Supabase URL is itself local.
… preview

Native bridge bugs

- platform.ts: stop caching isTabletViewport(). The viewport flips on
  rotation, iPad Slide Over, and Android freeform — a cached value lied
  about all of those. Re-evaluate on every call; reactive consumers read
  via useNative() which already re-snapshots on resize/orientationchange.
- network.ts: close the unmount-during-await leak. The Capacitor Network
  handshake is async (dynamic import + getStatus + addListener); if the
  component unmounted between awaits the cleanup ran first and the
  listener registered after. Now we track an `unmounted` flag at every
  await boundary and tear down a listener that resolves post-unmount.
- camera.ts: bitmap.close() in a finally block so cheap Android tablets
  don't leak GPU/native memory across repeated NCR captures. Also added
  double-settle guards on the file-input (Safari sometimes fires both
  cancel and change with no file).
- appShell.ts: hardware back now dispatches Escape first to close any
  open Radix dialog/sheet/popover/dropdown before popping the route.
  Operators with an OperationDetailModal open no longer get yanked off
  the page on the first back press.
- scanner.ts: fixed the TDZ trap where a pre-aborted signal called
  cleanup() before timeoutId / raf were declared. Reordered with let
  declarations + named function for cleanup so cleanup/onAbort can
  reference each other safely.

Mobile shell polish

- ScanFab.tsx: unified bottom offset across breakpoints (the bottom nav
  is rendered at every size, so the lg: override was wrong). Bumped
  z-index to 50 so the FAB stays above sticky page headers.
- MobileShell.tsx: dropped the redundant cn() + min-h-screen wrapper
  that was duplicating chrome height with operator pages and pushing
  scroll content under the safe-area inset. <main> already gets
  min-h-0 flex-1.

i18n parity

- Added the full mobile.* / filters.* / scanner.* / activity.* /
  workQueue.* / terminal.* key blocks to en/nl/de translation.json so
  Dutch and German operators no longer fall through to English fallback
  strings on every mobile screen.

STEP + PDF preview on mobile

- MobileOperationDetail.tsx: the file list now lazy-loads PDFViewer
  (~127 KB gz) and STEPViewer (~520 KB gz) on tap. iPad Pro M-series
  and modern Android tablets render the 3D viewer fluidly — full
  shop-floor inspection without walking to the desktop terminal. STEP
  files are pre-fetched as a Blob to dodge the supabase signed-URL CORS
  dance inside the Three.js loader. blob: URLs are revoked on close.

Tests

- src/native/platform.test.ts: 11 new tests covering Capacitor detection,
  iPhone vs iPad UA + touch-points heuristic, and (most importantly)
  that isTabletViewport() does NOT cache across rotation.
- src/native/scanner.test.ts: 3 new tests covering ScannerUnavailable
  fallback and pre-aborted-signal handling (no TDZ crash).

Docs

- docs/IOS.md: new "Compatibility (across all three apps)" section
  covering min OS, login flow, hosted vs. self-hosted backends, language
  switching, viewport handling, supported devices, and the lazy 3D/PDF
  viewer trade. Plus a "Remaining iOS-only gaps" checklist for what has
  to land in Xcode / Apple Developer before TestFlight (APNs key,
  Universal Links, splash + app icons, Info.plist localizations).

Validation: 772 tests pass (added 14), build clean, PWA emits 205
precached entries.
Build chain / chore

- tsconfig.app.json: 0 TS errors in the mobile + native surface (the 65
  pre-existing errors elsewhere are unrelated to this PR).
- vite.config.ts: switch to `manifest: false` so the hand-curated
  `public/manifest.webmanifest` is the single source of truth for PWA
  install metadata. The plugin no longer overwrites it at build time.
  Re-link `<link rel="manifest" href="/manifest.webmanifest">` from
  index.html so browsers find it.
- public/icons/: generated `icon-{192,512,maskable-192,maskable-512}.png`
  to satisfy the static manifest's icon paths (previously the install
  would 404 on icons).
- includeAssets: PWA precache now picks up the static manifest and the
  /icons/ set so they're available offline.

PWA install plumbing — end-to-end

- WorkQueue.tsx: the home-screen "Scan Job" shortcut points at
  /operator/work-queue?scan=1. Added a handler that pops the in-app
  ScanDialog when ?scan=1 is on the URL, then strips the param so a
  reload doesn't re-fire it. ?q= is also handled (pre-fills search after
  a successful scan).
- ScanDialog rendered directly on the operator work queue (used to be
  reachable only via the dead ScanFab).
- ScanFab.tsx: deleted. Was redundant with the bottom-tab "Scan" entry
  on the mobile shell and the new `?scan=1` handler — keeping it would
  have left a never-mounted component in the tree.

Native bridge

- src/native/push.ts: production-ready APNs / FCM registration helper.
  Honors permission state, falls back silently on the web, deduplicates
  registration listeners, and times out at 15s so it can't block the
  sign-in flow waiting on the network.
- AuthContext.tsx: registerPushNotifications() is now invoked on the
  SIGNED_IN auth event so the device token handshake happens on first
  authenticated launch. Backend dispatch isn't wired yet (documented as
  a remaining gap in IOS.md / ANDROID.md), but the iOS / Android side is
  ready end-to-end.
- MobileQueue.tsx: split-view decision now uses `useTabletLayout`
  (viewport-based) instead of UA-based `isIPad`. iPad Slide Over /
  Android freeform / split-screen now correctly collapse to a
  single-column shell, where the previous UA fingerprint lied.
- platform.ts: replaced the type-only Capacitor import with the proper
  `typeof CapacitorBridge` typing so `tsc --noEmit` passes cleanly.

Tests

- src/components/mobile/MobileShell.test.tsx: render smoke test —
  asserts the bottom tab bar renders on /m/queue and is hidden on the
  full-screen /m/scan and /m/login flows. Mocks the platform / theme
  context dependencies so it doesn't need a real Supabase environment.

Docs

- docs/ANDROID.md: mirrored the IOS.md compatibility table (min OS,
  auth, hosted vs self-hosted, language, viewports, devices) plus a
  "Login flow", "PDF + STEP CAD preview", "PWA install — manifest of
  record", and "Hardware back / dialog dismissal" sections so the
  Android side has the same depth of documentation as iOS.

Validation: 775 tests pass (added 3), build clean, PWA emits 210
precached entries, static manifest ships unmodified to dist/, all four
icon paths the manifest references actually exist.
New top-level reference at docs/DEPLOY_AND_TEST.md covering all three
surfaces end-to-end. Cross-linked from README.

Each surface gets its own section:

  1. PWA — local dev, prod build, hosted deploys (Vercel + Cloudflare
     wired in CI), how to install on each desktop/mobile platform,
     install verification checklist, Lighthouse PWA audit target,
     CI pipeline.
  2. iOS / iPadOS — Xcode + signing prereqs, the npm run ios:init
     bootstrap, simulator + physical-device test matrix, the exact
     flows to exercise (cold launch, sign-in, PIN, biometric, scanner,
     pull-to-refresh, swipe actions, timer, offline banner, PDF + STEP
     preview, push registration, status bar, modal dismissal),
     TestFlight upload, App Store submission, links to the remaining
     gaps.
  3. Android — Studio + SDK + Java 17 prereqs, npm run android:open
     bootstrap, recommended emulators (Pixel 6/8 Pro/Tablet, Galaxy Tab,
     Z Fold 5), physical-device test matrix with the Android-specific
     flows (hardware back through OperationDetailModal, system gesture,
     Material You themed icon, split-screen, network security config,
     FCM push), AAB build and Play Console rollout.

Plus a cross-cutting test matrix (vitest, tsc, eslint, vite build,
api e2e, capacitor sync, on-device, Lighthouse, SW offline) and a
release-readiness checklist that covers the manifest reconciliation
(static file is the source of truth; vite-pwa runs in `manifest: false`
mode), the four /icons/ PNGs that ship, the SW prompt-mode update
contract, version bumps for native shells, and the `?scan=1` shortcut
handshake.

Closes the deploy / test gap that an operator pretending to ship this
end-to-end would otherwise hit.
…lback

Three real architectural bugs from the cross-app audit. None were caught
by lint / tsc / tests; the failure modes only show up at runtime.

A. Push registration fires on every SIGNED_IN event — including token
   refresh, browser tab restore, and Supabase replaying the most recent
   event to a newly mounted listener. Operators on iOS would see the
   APNs permission sheet re-pop on every cold start, and we'd fire a
   registration handshake that's idempotent but wasted.

   Fix in `AuthContext.tsx`: track the last user id we registered for in
   a closure-scoped variable. Only fire `registerPushNotifications()`
   when the *user id* changes, not on every SIGNED_IN.  Reset the
   tracker on TOKEN_REFRESHED-failure and SIGNED_OUT. Token refreshes
   no longer trigger a register call.

B. The PWA service worker registers inside the Capacitor WebView too.
   `useRegisterSW()` calls `navigator.serviceWorker.register()` on mount.
   On Android (Chrome-based WebView) this would install a SW for the app
   origin and start intercepting Supabase calls when running with
   CAPACITOR_SERVER_URL=https://...

   Fix in `PwaUpdatePrompt.tsx`: split into a thin gate component that
   bails out via `isNativeApp()` *before* the `useRegisterSW` hook runs
   (rules of hooks: the hook is in the inner component that's only
   mounted on the web). iOS's WKWebView ignores SWs entirely so this is
   defense-in-depth there; on Android it's the difference between a
   working app and one that gets its API calls hijacked by an old SW.

C. There is no `appUrlOpen` handler — magic-link / OAuth callback URLs
   on iOS / Android can't get the auth tokens back into the WebView. The
   user taps a magic link in their email, the OS routes it to the app
   via Universal Link or URL scheme, and the URL was being dropped.

   Fix in `src/native/appShell.ts`: register a Capacitor App.appUrlOpen
   listener inside `initNativeShell()`. Pulls `access_token` /
   `refresh_token` out of the URL fragment and calls
   `supabase.auth.setSession()` for magic-link + recovery flows; pulls
   `code` out of the query string and calls
   `supabase.auth.exchangeCodeForSession()` for the OAuth PKCE flow.
   Logs unrecognised callbacks to `adb logcat` / Xcode console at warn
   level instead of crashing.

   This is the missing piece that makes auth work end-to-end on iOS +
   Android. The Apple Developer / Google Play side still needs the
   Universal Link entitlement / app-links verification configured (see
   docs/IOS.md "Remaining iOS-only gaps") — those are out-of-band
   per-tenant config, not code.

Verifications:
- `npm run test:run` — 775 tests pass.
- `npm run build` — clean. Main bundle has 0 references to `@capacitor`
  (the bridge boundary still holds; every plugin is dynamically imported
  into its own chunk).
Self-hosted, on-prem deployment is the primary path for Eryxon Flow
(most shops run their own Supabase + their own LAN host). The deploy
guide previously led with App Store / Play Store / TestFlight, which
sells SaaS distribution as the canonical workflow when it's actually
the rare case.

- docs/DEPLOY_AND_TEST.md: new "Self-host install paths at a glance"
  matrix at the top. Each surface section now leads with the
  no-store-required self-host path: cable-install via Xcode (free or
  Ad Hoc) for iOS, `adb install` / hosted APK / MDM push for Android,
  in-browser PWA install for web. The App Store / Play Store sections
  are kept but explicitly framed as "only if you ship as SaaS".
- docs/DEPLOY_AND_TEST.md: explicit recipes for pointing each native
  shell at a LAN host — both `VITE_SUPABASE_URL=http://shop-floor.local`
  baked-in builds and `CAPACITOR_SERVER_URL=http://shop-floor.local`
  live-reload.
- scripts/ios-init.sh: also auto-registers the `eryxon://` URL scheme
  in `CFBundleURLTypes` so magic-link callbacks work on iOS without
  needing Universal Links + an Apple App Site Association on a public
  host (which self-hosters can't provide). The handler in
  `src/native/appShell.ts` already accepts both `eryxon://auth/...` and
  `https://app.eryxon.eu/auth/...`. Same script also seeds
  `CFBundleDevelopmentRegion=en` + `CFBundleLocalizations=[en,nl,de]`
  so the App Store listing fields are correct on the rare SaaS path.
- Android: the `eryxon://` intent-filter was already in
  `android/app/src/main/AndroidManifest.xml` from the original Android
  PR — no changes there, just documented.
Eryxon Flow is primarily self-hosted. Operators run their own Supabase
+ app stack on a plant-internal host (`http://shop-floor.local:8080`,
`http://192.168.1.50:54321`, etc.) — usually over plain HTTP because
the shop hasn't fronted it with TLS yet. Without these fixes, every
surface had at least one block in that path.

Four real LAN-blocking gaps. All closed.

1. Meta CSP didn't include the configured Supabase host
   - vite.config.ts: applyCspBuildRewrites() now injects the origin from
     `VITE_SUPABASE_URL` (and the matching ws:/wss: variant) into the
     `connect-src` directive at build time. Verified with three shapes:
       LAN hostname  → http://shop-floor.local:54321 ws://shop-floor.local:54321
       LAN IP        → http://192.168.1.50:54321     ws://192.168.1.50:54321
       Cloud Supabase → https://abc.supabase.co       wss://abc.supabase.co
   - The dev-only localhost strip is preserved when the configured URL
     itself is local (Docker self-host pattern).
   - Bug fix on the regex: the negation set excluded single quotes,
     which ended the match inside `'self'` and meant the injection
     silently no-op'd. Switched to `[^";]` since the CSP value is
     wrapped in double quotes.

2. Android `network_security_config.xml` only whitelisted two /24s
   - Broadened to the full IETF private-use space (RFC 1918):
     10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 — enumerated as
     individual <domain> entries because Android NSC doesn't support
     CIDR. Plus link-local (169.254.0.0/16), the conventional LAN
     suffixes (.local, .lan, .home, .internal, .corp, .private,
     .intranet), and emulator loopbacks (10.0.2.2, 10.0.3.2).

3. iOS App Transport Security was blocking every plain-HTTP LAN call
   - scripts/ios-init.sh now patches Info.plist on `npm run ios:init`:
       - NSAppTransportSecurity.NSAllowsLocalNetworking = true
         (Apple-blessed escape valve for RFC1918 + .local; doesn't
         require an App Store review justification).
       - NSAppTransportSecurity.NSAllowsArbitraryLoadsInWebContent = true
         so the WebView can load HTTP assets from the LAN host (CSS,
         fonts, images).
       - NSBonjourServices = [_http._tcp, _https._tcp] so iOS 14+
         resolves shop-floor.local without silently dropping it.
       - NSLocalNetworkUsageDescription so the OS-level local-network
         permission sheet shows the right reason.

4. Caddyfile only had a public-domain ACME profile
   - Rewrote with three named sections:
       A. Public host via Let's Encrypt (the original — kept, commented).
       B. LAN host via `tls internal`. Caddy issues a self-signed cert
          from its own CA so the PWA install prompt fires and the
          Service Worker can register on `https://shop-floor.local`.
          Operators install the Caddy root once per device. Driven by
          $LAN_HOST and $LAN_IP env vars (defaults to shop-floor.local).
          Adds Service-Worker-Allowed: /, no-cache headers for /sw.js +
          /env.js, immutable Cache-Control for /assets/*.
       C. Pure-HTTP LAN (no TLS at all) — explicitly documented as
          discouraged because the SW won't register, but called out so
          self-hosters who refuse TLS know native Capacitor builds
          still work via the network_security_config / ATS exceptions
          above.

Validation: 775 tests pass, build clean, CSP rewrite verified with the
three LAN deploy shapes above.
Migrate AI tooling to Codex.

- AGENTS.md: symlink -> regular file (Codex doesn't follow symlinks)
- .agents/README.md: update for Codex
- .agents/skills/: add explore-code, source-command-{explore,graph-status,interrogate}
- .codex/: add 8 subagents (code-explorer, dependency-analyzer, explain-service, find-usages, opentrace, repo-ops, supabase-db, tech-stack) + config.toml + migration report
- src/lib/config.ts: add isSelfHosted() helper + config.test.ts
- src/test/setup.ts: jsdom configurable fix
- .gitignore: broaden .env.* coverage, add .claude/worktrees/, supabase/.branches/
Consolidates AGENTS.md regular-file conversion, .codex/ subagents,
src/lib/config.ts isSelfHosted helper, jsdom setup fix, and .gitignore
hardening from #581 into this release PR.
… (ERY-34)

AuthContext.fetchProfile never selected the onboarding columns, so
App.tsx's gate `profile.onboarding_completed === false` evaluated
`undefined === false` and no admin was ever routed into the wizard;
tenant-level completion was never stamped.

- AuthContext: select onboarding_completed + onboarding_step, coerce
  nullable columns to false/0, add refreshProfile() for re-hydration.
- App.tsx: route only incomplete admins into the wizard
  (needsOnboarding = role === "admin" && onboarding_completed === false);
  operators are never gated into the admin-only flow.
- OnboardingWizard: on complete and skip, stamp
  tenants.onboarding_completed_at = now() and refreshProfile() before
  navigating away.

Verification: vitest (20 passed, 3 new) + tsc --noEmit clean.
Board-approved to land (approval dde8da46).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… (ERY-34)

AuthContext.fetchProfile never selected the onboarding columns, so
App.tsx's gate `profile.onboarding_completed === false` evaluated
`undefined === false` and no admin was ever routed into the wizard;
tenant-level completion was never stamped.

- AuthContext: select onboarding_completed + onboarding_step, coerce
  nullable columns to false/0, add refreshProfile() for re-hydration.
- App.tsx: route only incomplete admins into the wizard
  (needsOnboarding = role === "admin" && onboarding_completed === false);
  operators are never gated into the admin-only flow.
- OnboardingWizard: on complete and skip, stamp
  tenants.onboarding_completed_at = now() and refreshProfile() before
  navigating away.

Verification: vitest (20 passed, 3 new) + tsc --noEmit clean.
Board-approved to land (approval dde8da46).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ersistence (ERY-46)

Add the managed-pilot observability baseline so a single request can be traced
from edge logs into a durable activity_log row via a shared request_id.

- New _shared/observability.ts: resolveRequestId (trust valid inbound
  x-request-id, else mint), structured edgeLog, shouldPersistPilotEvent filter,
  and persistPilotEvent writing request_id into activity_log.metadata.
- createApiHandler wraps every request: boundary request-id, x-request-id on all
  responses, request lifecycle logs, automatic edge.error persistence, and a
  ctx.recordPilotEvent hook for success-path lifecycle events.
- errorHandler: extracted mapError as single error->code/status taxonomy;
  handleError echoes x-request-id. CORS exposes x-request-id.
- crud-builder gains opt-in onCreated hook; api-issues records issue.created.
- Frontend LogContext extended with the contract fields; docs updated.
- Tests: src/lib/__tests__/edgeObservability.test.ts (12) cover request-id
  propagation and the persistence filter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ERY-48)

Packages the ERY-41 manual restore drill into one engineer-owned command:
schema-scoped pg_dump (public/auth/storage) -> storage-volume tar snapshot
-> restore into a disposable scratch DB -> extract -> parity + smoke checks.

Dump/restore run inside the supabase db container so host pg_dump version
drift does not break the drill; read-only checks use host psql.

Smoke beyond row counts: sample-file SHA-256 parity, referential integrity
(orphan parts/jobs), RLS helper presence (get_user_tenant_id), pilot-tenant
shape. Auto-cleans scratch DB + temp on success; --keep/--log flags.

- scripts/restore-drill.sh
- docs/BACKUP_RESTORE_DRILL.md
- package.json: npm run drill:restore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
…aths (ERY-51)

Turn the ERY-46 observability baseline on across the real pilot paths.

- api-job-lifecycle, api-operation-lifecycle: migrate from raw `serve` onto
  `serveApi`/`createApiHandler`, so each request carries a boundary
  x-request-id, structured edge logs, and automatic edge.error persistence.
  Record job.lifecycle / operation.lifecycle / operator.time_entry pilot
  events via ctx.recordPilotEvent, and forward x-request-id into the
  webhook-dispatch hop.
- webhook-dispatch, mqtt-publish: keep internal-secret auth but resolve the
  request id at the boundary, echo x-request-id on every response, and persist
  webhook.dispatch_failed / mqtt.dispatch_failed (per-delivery + top-level)
  into activity_log with the shared id.
- AuthContext: auth.session_recovery and auth.tenant_switch carry the logger
  contract (eventType + service) on success and failure.
- TerminalLogin, MobileLogin: operator.login success/reject/error events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
….6 (ERY-71)

- ML Kit barcode: wire the dead mlkitBarcodeDependencies placeholder into a real
  com.google.mlkit.vision.DEPENDENCIES manifest meta-data (install-time model
  delivery); document the air-gapped bundled-model build path in docs/ANDROID.md.
- network_security_config.xml: replace the fabricated sample-IP RFC1918 cleartext
  list (false coverage, no CIDR matching) with a least-privilege strategy —
  release builds HTTPS-only, dev cleartext confined to debug-overrides, plain-HTTP
  LAN hosts supported per-host via a documented custom release build or TLS fronting.
- Align version surfaces to 0.5.2 (build.gradle + README) with package.json/CHANGELOG.
- Add docs/2026-05-24-ery-71-native-packaging-validation.md with the static checks run.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
- App.tsx home routing: native-shell admins were gated to /m/queue
  because the admin branch required !native.isNative. Native admins
  now land on /admin/dashboard as the comment intends; operators and
  mobile-shell users still route to /m/queue.
- pilot-alert-evaluator: migrate deprecated std@0.168.0 serve() to the
  Deno.serve() entrypoint (matches other edge functions). The old import
  fails to start on the current Supabase runtime, breaking the pg_cron job.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3bd21bbb1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 193 to +194
.eq("status", "completed")
.gte("completed_at", startDate)
.lte("completed_at", endDate);
.is("deleted_at", null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict completed job count to requested period

get_production_metrics now counts completed jobs without applying start_date/end_date, so the jobs.completed metric returns all-time totals instead of period totals. Any dashboard or automation that compares periods (for example, last 7 days vs last 30 days) will get inflated, non-comparable results even though the response advertises a bounded period.

Useful? React with 👍 / 👎.

Comment thread src/contexts/AuthContext.tsx Outdated
Comment on lines +125 to +126
if (event === 'SIGNED_OUT') {
lastRegisteredUserId = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset push-registration cache when a user signs out

The SIGNED_OUT reset branch is nested inside if (session?.user), but SIGNED_OUT events provide no session user, so lastRegisteredUserId is never cleared on normal sign-out. After signing out and back in as the same user during the same app runtime, push registration is skipped because the stale user id still matches, which can leave notification setup stale after logout/login cycles.

Useful? React with 👍 / 👎.

SheetMetalConnect and others added 6 commits May 25, 2026 14:25
… privacy

Marketing-layer overhaul on the v0.6 line (EN / NL / DE):

- Localisation/humanizer pass on all marketing copy. The NL was literal AI
  translation; rewritten to read like a Dutch metalworker/owner wrote it
  ("Grip op je orders, van de vloer tot de planning" over "Een rustige,
  open-source MES voor jobshops die leveren"). DE rewritten as natural
  Hochdeutsch, EN sharpened. Dropped the "calm MES" framing and AI vocab;
  kept trade terms (jobshop, MES, snijden/kanten/lassen).

- Roadmap page redesigned from a bare Canny iframe into our own three-column
  status board (Shipped / In progress / Planned) on the --ery-* token
  contract, with real items and status accents from the semantic state
  tokens. Canny is paired in as a "vote on the public board" CTA. Native
  iOS / Android sit under "In progress" as in-development work.

- Imprint (/imprint/) + Privacy (/privacy/) pages, design-system-styled via a
  shared Legal.astro. Entity data (Sheet Metal Connect e.U., Wien AT) taken
  1:1 from vanenkhuizen.com; nothing fabricated. Both carry a prominent
  Apache 2.0 "AS IS" / use-at-your-own-risk disclaimer. Linked locale-aware
  from the footer.

No version/status bloat on any marketing page (versions stay in the release
notes). Build green; all 15 marketing/legal pages (5 x EN/NL/DE) return 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Luke: keep only how-to-use / how-it-works docs, no snapshot garbage.

- Delete guides/release-proof-v0-5-1.md (historical proof snapshot)
- Remove its sidebar entry from Getting Started
- Drop the dangling changelog link to the deleted page
- Strip the 'v0.5.1 proof snapshot is historical context' orientation
  prose from deployment.md and self-hosting.md leads
- Tidy 'release proof' wording in articles/index.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Luke: versions belong only in the changelog, not in the docs. Docs
explain how the product works and is used, without version-status
enumerations.

- Remove the 'Where things stand / Huidige status / Stand heute'
  status blockquotes from introduction (EN/NL/DE)
- Remove the standalone 'Current Status' Section and the
  'Stable (v0.6) / On the roadmap' status pills from index.mdx (EN/NL/DE)
- De-version the 'One Platform, Every Screen' cards and section copy
- Drop the 'May 2026 status' version-matrix lead from roadmap.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… tighten FOSS framing

The README no longer carries a dated 'Project Status' block or a version
badge — version and release state belong in the changelog. Reworded the
intro to plain FOSS framing (Apache 2.0, self-hosted MES for metalworking
job shops) and removed the dead GEMINI.md row from the AI agent table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45637cd23d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mcp-server/src/index.ts Outdated
// Tool handlers call supabase.from() directly, so they need the raw client,
// not the UnifiedClient wrapper which only exposes .select()/.insert()/etc.
const unifiedClient = createClient(config);
const supabaseClient = (unifiedClient as DirectSupabaseClient).getSupabaseClient();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve tenant scoping when wiring MCP Supabase client

Switching to getSupabaseClient() bypasses the DirectSupabaseClient wrapper that applies TENANT_ID filtering on every query, so tools now execute raw service-role .from(...) calls without automatic tenant constraints. In any deployment with multiple tenant rows (or operators expecting TENANT_ID isolation), this can leak or mutate cross-tenant data because the server now hands unscoped admin access to all tool handlers; keep requests routed through the scoped wrapper (or re-apply equivalent tenant enforcement before execution).

Useful? React with 👍 / 👎.

SheetMetalConnect and others added 2 commits May 25, 2026 14:42
…o footer

- Remove /pricing/ + /[locale]/pricing.astro; nav 'Pricing' now scrolls to inline #pricing on the landing
- Drop redundant 'see all pricing' link on the landing
- Add LinkedIn + email contact to the footer creator card

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… pages

Docs IA was carrying two redundant navigation layers: the left sidebar
group tree AND a horizontal secondary tab-bar (SidebarNav) that listed the
same 8 group headings. Removed the tab-bar so docs run a single nav source —
minimal top nav + left sidebar. The language switcher it hosted moved into
the header next to search + theme, keeping locale switching on docs pages.

Footer: the Starlight docs footer was suppressed on docs pages and the menu
config still held retired contacts. The footer now renders on docs pages with
the marketing IA (brand + Apache 2.0, Code & Community, Resources, Contact)
and the Contact column is cleaned across en/nl/de — dropped office@vanenkhuizen,
the Managed rollout page link, and the vanenkhuizen.com promo; contact is now
Luke's LinkedIn + luke@sheetmetalconnect.com, with locale-aware Imprint/Privacy.

Legal pages (Legal.astro, EN/NL/DE): redesigned from a flat single card into a
calm two-column layout — sticky publisher/controller identity rail beside a
readable legal column, hairline rows, mono VAT/Firmenbuch identifiers, and the
Apache 2.0 "AS IS" disclaimer as a warning-accented callout. All statutory
fields preserved verbatim.

Verified: npm run build green (131 pages), tokens:check passes, astro check
clean for changed files (4 remaining errors are pre-existing in untouched
Hero/Search/ListCard). HTTP 200 on docs + imprint/privacy EN/NL/DE; docs pages
show no tab-bar, footer present, no office@vanenkhuizen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…inline on landing)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f64cbb3a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mcp-server/src/tools/substeps.ts Outdated
operation_id,
name,
sequence: finalSequence,
status: "pending",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a valid status value when creating substeps

add_substep now inserts status: "pending", but the substeps table constraint only allows not_started, in_progress, completed, or blocked (see schema in supabase/migrations/20260121175020_remote_schema.sql). This means every call to this tool will fail at insert time with a check-constraint error instead of creating the substep.

Useful? React with 👍 / 👎.

Comment on lines +55 to +59
// Note: jobs table only has status, created_at, updated_at — no dedicated timestamp fields
const { tool: startJobTool, handler: startJobHandler } = createStatusTransitionTool({
tableName: 'jobs',
toolName: 'start_job',
description: 'Start a job (changes status to in_progress and tracks start time)',
description: 'Start a job (changes status to in_progress)',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore lifecycle timestamp writes for MCP job transitions

The transition configs no longer set lifecycle timestamp fields, so start_job/stop_job/complete_job/resume_job now only mutate status and leave started_at, paused_at, completed_at, and resumed_at unset when jobs are changed via MCP. Those columns still exist in the schema and are used by lifecycle/metrics flows, so MCP-driven job changes will produce incomplete lifecycle data and can be excluded from time-based reporting.

Useful? React with 👍 / 👎.

SheetMetalConnect and others added 2 commits May 25, 2026 15:18
The /roadmap/ surface (EN/NL/DE) now shows the live public Canny board
instead of a hand-maintained, invented list. Read-only: the site pulls
from Canny at build time and renders it in the existing design system.
Luke manages the board in Canny's own UI; there is no push/sync-back.

- src/lib/canny.ts: build-time, server-side-only loader. Reads
  CANNY_API_KEY from the build env (never the browser bundle). Groups
  posts into Canny's own statuses (under review / planned / in progress /
  complete). Never throws — missing key, network, or rate-limit failures
  fall back to an honest empty state so the build stays green. Admin URLs
  are rewritten to public visitor URLs.
- Roadmap.astro: four status columns from the live board, per-item title +
  first-line detail + vote/comment counts + category, empty-column and
  empty-board states, Canny link-out CTA. Shared --ery-* tokens only.
- marketing-content.ts: removed all hardcoded roadmap items; keeps only
  locale chrome (hero, column labels, empty/meta copy, vote CTA). Column
  statuses now map 1:1 to Canny. CANNY_ROADMAP_URL points at the real
  public board (eryxon-flow.canny.io/eryxon).
- .env.example: CANNY_API_KEY placeholder (real key stays in gitignored
  .env, server-side only).

Verified: build green with key present (live data in dist), no key in
dist/, fallback build green without key. Dev server /roadmap/ EN/NL/DE → 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…emplate width

The container width/centering was scoped to MarketingLayout, so component-level
pages (roadmap, imprint/privacy, pricing) rendered full-bleed with no page width.
Promoting it to a :global() utility makes every marketing surface inherit the
same design-system container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af31cb100a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to +35
description: 'Start an operation (changes status to in_progress)',
newStatus: 'in_progress',
timestampField: 'started_at',
additionalFields: { paused_at: null },
validTransitions: {
'not_started': ['in_progress'],
'on_hold': ['in_progress'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore MCP operation lifecycle timestamp writes

start_operation now only sets status to in_progress and no longer stamps started_at (and pause_operation similarly no longer stamps paused_at). When operations are transitioned through MCP instead of the dedicated lifecycle endpoints, those records lose lifecycle timing data that downstream time-tracking/metrics flows rely on, so MCP-driven runs become inconsistent with the rest of the system.

Useful? React with 👍 / 👎.

- add_substep inserted status "pending", which substeps_status_check rejects
  (allows not_started/in_progress/completed/blocked) — every call failed.
  Use "not_started" for a newly created substep.
- Tool handlers call supabase.from() directly on the service-role client,
  which bypasses RLS. When TENANT_ID is set, route them through a scoped
  client wrapper that constrains every query to that tenant (no-op when
  TENANT_ID is unset, i.e. the default single-tenant self-host).

Not addressed: Codex P2 on jobs.ts lifecycle timestamps is a false positive
— jobs has no started_at/paused_at/completed_at/resumed_at columns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sign-out

- get_production_metrics counted completed jobs all-time (jobs has no
  completion timestamp), making period comparisons non-comparable. Bound by
  updated_at as the completion proxy, matching the operations query.
- AuthContext: the SIGNED_OUT reset of lastRegisteredUserId sat inside the
  `if (session?.user)` branch, but SIGNED_OUT carries no user, so the cache was
  never cleared — re-login as the same user skipped push registration. Move the
  reset into the no-user branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@SheetMetalConnect
Copy link
Copy Markdown
Owner Author

v0.6 release notes — scope + review triage

Scope: v0.6 ships the new website, installable PWA, and stability fixes. Native (iOS/Android) is explicitly out of scope and deferred to a later branch (v0.7+). The iOS bootstrap + Android smoke check is therefore expected-red here and is not a release blocker for v0.6 — the native shell is not part of this release.

Codex review triage (all 8 findings):

  • substeps.ts invalid "pending" status (P1) — fixed → not_started.
  • index.ts MCP tenant scoping (P1) — fixed: service-role client now tenant-scoped when TENANT_ID is set (no-op for default single-tenant self-host).
  • dashboard.ts all-time vs period jobs.completed (P1) — fixed: bounded by updated_at (jobs has no completion timestamp).
  • AuthContext.tsx push-cache not cleared on sign-out (P2) — fixed: reset moved to the no-user branch.
  • ☑️ App.tsx native admin routing (P1) — already correct in HEAD (admin routes to /admin/dashboard unconditionally).
  • ⏭️ jobs.ts / operations.ts lifecycle timestamps (P2 ×2) — false positives: those tables have no started_at/paused_at/resumed_at columns; adding writes would break inserts. Current code stamps only the columns that exist (completed_at).
  • ⏭️ pilot-alert-evaluator "use Deno.serve" (P1) — false positive: already uses Deno.serve() (index.ts:66).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22c93dd41f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +103 to +107
open: issues.filter(
(issue) => issue.status === "open" || issue.status === "acknowledged",
).length,
resolved: issues.filter(
(issue) => issue.status === "resolved" || issue.status === "rejected",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align mobile issue status filters with current issue enum

This screen filters and labels issues using open/acknowledged/resolved/rejected, but the app’s generated Supabase types define issue_status as pending/approved/rejected/closed (src/integrations/supabase/types.ts). As a result, newly created pending issues are omitted from the “Open” tab and approved/closed issues are omitted from “Resolved”, so operators see incorrect counts and missing records on the mobile issues page.

Useful? React with 👍 / 👎.

Comment thread src/pages/mobile/MobileIssueSheet.tsx
@SheetMetalConnect SheetMetalConnect self-assigned this May 25, 2026
@SheetMetalConnect SheetMetalConnect merged commit 6ea558d into main May 25, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants