# Frontend cutover (Etap 9-11) — apply only after Etap 8 counts match Two changes to `public/app-runtime.js`, both required together. Do not commit either until `scripts/03-verify-counts.sql` shows matching counts on old vs new (14/11/53/52/460/18) and the Etap 10 smoke test has passed against `https://api.caterium.ru` directly (before touching the frontend). ## 1. Point the default at the new backend ```js // public/app-runtime.js, line ~1989-1990 const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru'; const DEFAULT_SUPABASE_KEY = ''; ``` ## 2. Auto-migrate devices that already have the OLD url cached in `localStorage` Every Caterium device has `sunCloudV2Config` (and possibly the legacy `sunEnterpriseSettingsV1`) in `localStorage`, which `loadConfig()` reads *before* falling back to `DEFAULT_SUPABASE_URL`. Just changing the default does nothing for those devices — they keep talking to `cksuehzcimitsxmeloes.supabase.co` forever unless we rewrite the stored value once. Insert this right after the existing `DEFAULT_SUPABASE_KEY` line, and call it from inside `loadConfig()` before the `return { ... }`: ```js const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru'; const DEFAULT_SUPABASE_KEY = ''; const LEGACY_SUPABASE_URL = 'https://cksuehzcimitsxmeloes.supabase.co'; function migrateLegacySupabaseUrl(value) { // One-time, silent migration: any device still pointed at the retired managed // Supabase project is switched to the self-hosted backend automatically. Users // are never asked to clear localStorage or reconfigure anything. if (String(value.url || '').trim().replace(/\/$/, '') === LEGACY_SUPABASE_URL) { value.url = DEFAULT_SUPABASE_URL; value.key = DEFAULT_SUPABASE_KEY; } return value; } ``` Then in `loadConfig()`, wrap the legacy-settings merge: ```js function loadConfig() { let value = {}; try { value = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}') || {}; } catch (_) {} if (!value.url || !value.key) { try { const legacy = JSON.parse(localStorage.getItem('sunEnterpriseSettingsV1') || '{}') || {}; const cloud = legacy.cloud || {}; value.url ||= String(cloud.supabaseUrl || '').trim(); value.key ||= String(cloud.anonKey || '').trim(); } catch (_) {} } value = migrateLegacySupabaseUrl(value); // <-- add this line return { url: String(value.url || DEFAULT_SUPABASE_URL || '').trim().replace(/\/$/, ''), key: String(value.key || DEFAULT_SUPABASE_KEY || '').trim(), ... ``` `saveConfig()` (a few lines below) persists `config` back to `localStorage` on every config change already, so the corrected URL/key get written back to `sunCloudV2Config` the first time `loadConfig()` runs on each device — no explicit persistence call needed here, but if a save is not triggered naturally during your smoke test, call `saveConfig()` once right after `config = loadConfig();` at module init (line ~1992) to force-persist the migration immediately rather than waiting for the next unrelated write. ## 3. Session invalidation is expected Because the new stack uses a freshly generated `JWT_SECRET` (README decision, since the old one can't be safely retrieved), any cached Supabase auth session (JWT) in `localStorage` becomes invalid the moment the client points at the new URL — `supabase-js` will see a 401 on its first call, clear the stale session automatically, and the user lands back on the Caterium login screen. That matches the task's accepted trade-off: users log in once more with their existing email + password (unchanged, since `auth.users.encrypted_password` is copied byte-for-byte). No code change needed for this part — it's `supabase-js`'s default behavior. ## Checklist before running this cutover - [ ] `scripts/03-verify-counts.sql` matches old vs new (14/11/53/52/460/18) - [ ] `curl https://api.caterium.ru/rest/v1/`, `/auth/v1/`, `/storage/v1/`, `/functions/v1/` all respond (not connection errors) - [ ] Realtime WebSocket connects: `wscat -c wss://api.caterium.ru/realtime/v1/websocket?apikey=` - [ ] `scripts/05-storage-sync.sh` finished, spot-checked photo URLs load - [ ] `scripts/04-deploy-edge-function.sh` shows `caterium-create-employee` rejecting an unauthenticated call with 401/400 (not 5xx) - [ ] Manual login test against `https://api.caterium.ru` directly (temporarily point a local dev copy of `app-runtime.js` at it) with one real existing account, confirm orders/ clients/catalog load Only once every box above is checked: apply the two edits above, commit to `main` with a message like `Cutover: point Caterium at self-hosted Supabase (api.caterium.ru)`, push. CI (`Caterium QA` -> `Caterium Promote Production`) auto-promotes `main` to `production` on green, and the existing Timeweb cron (`ops/timeweb/caterium-production-sync.sh`) deploys it to `app.caterium.ru` within 5 minutes. Do NOT skip CI (`--no-verify` or manual production-branch pushes) for this change — the QA workflow's Playwright e2e run is exactly the safety net you want on a backend cutover. After confirming `https://app.caterium.ru` works end-to-end with no `supabase.co` requests in DevTools Network, leave the old managed Supabase project (`cksuehzcimitsxmeloes`) running untouched as a cold backup, per the task's explicit instruction — do not pause or delete it without separate owner confirmation.