fix: make LifeOS actually render in local mode
The container served a blank page. Four upstream assumptions broke a
root-served, Supabase-less deployment; all are patched at build time with
grep assertions so a silent upstream rename fails the build instead of
shipping a blank page again.
- React Router was pinned to basename '/app' (upstream is hosted under an
/app path). At '/' it matched no route and rendered nothing -- silently,
with only a console warning. This was the blank page.
- supabase.ts calls createClient() at module scope, which throws
'supabaseUrl is required' on an empty string and killed the bundle before
React mounted. Dummy values fix it; nothing reads Supabase in local mode.
- initAuth() only booted the local user for Tauri/Electron, so a browser
build fell through to a Supabase login screen the backend cannot serve
(it has no /api/auth/login or /api/auth/signup -- it is single-user).
- The local API adapter has no realtime layer, so supabase.channel() was
undefined and every subscribing component tripped the ErrorBoundary
('Something went wrong'). Added no-op stubs; realtime is meaningless on a
single-user instance.
Verified in a browser: dashboard and routed pages render, 0 fatal errors.
Known gap: the Flask backend implements only a subset of the cloud schema,
so quests, messages, partnerships, grocery_lists, recurring_transactions,
junction_traditions and habit_suggestions 404. Non-fatal; those features
degrade. Core (tasks/habits/goals/health/finance/journal/XP/AI chat) works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,41 @@ RUN npm ci --ignore-scripts
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Upstream is hosted under an /app base path and hardcodes it in two places.
|
||||||
|
# We serve at the root, so both must be rewritten or the app renders a blank page:
|
||||||
|
# - App.tsx pins React Router to basename "/app"; at "/" it matches no route and
|
||||||
|
# renders NOTHING (silent -- only a console warning, no error).
|
||||||
|
# - index.html points the PWA manifest and touch icons at /app/...
|
||||||
|
RUN sed -i 's|basename: "/app"|basename: "/"|' src/App.tsx \
|
||||||
|
&& sed -i 's|"/app/|"/|g' index.html \
|
||||||
|
&& grep -q 'basename: "/"' src/App.tsx \
|
||||||
|
&& ! grep -q '"/app/' index.html
|
||||||
|
|
||||||
|
# initAuth() only boots the single local user for Tauri/Electron, so a browser
|
||||||
|
# build in local-api mode falls through to a Supabase login screen instead --
|
||||||
|
# one the backend cannot serve (it has no /api/auth/login or /api/auth/signup
|
||||||
|
# routes, being single-user by design). Treat local-api like the desktop envs.
|
||||||
|
RUN sed -i "s@if (detectedEnv === 'tauri' || detectedEnv === 'electron') {@if (detectedEnv === 'tauri' || detectedEnv === 'electron' || detectedEnv === 'local-api') {@" src/stores/useUserStore.ts \
|
||||||
|
&& grep -q "detectedEnv === 'local-api') {" src/stores/useUserStore.ts
|
||||||
|
|
||||||
|
# The local API adapter has no realtime layer, so supabase.channel() is undefined
|
||||||
|
# and every subscribing component crashes the app. Append no-op stubs.
|
||||||
|
COPY local-api-realtime-shim.ts /tmp/shim.ts
|
||||||
|
RUN cat /tmp/shim.ts >> src/lib/local-api.ts && rm /tmp/shim.ts
|
||||||
|
|
||||||
# Routes the data layer (src/lib/data-access.ts) to the local Flask API instead
|
# Routes the data layer (src/lib/data-access.ts) to the local Flask API instead
|
||||||
# of Supabase Cloud. VITE_API_BASE_URL is deliberately left unset: local-api.ts
|
# of Supabase Cloud. VITE_API_BASE_URL is deliberately left unset: local-api.ts
|
||||||
# then falls back to a relative /api, so the bundle works on any host and port.
|
# then falls back to a relative /api, so the bundle works on any host and port.
|
||||||
ENV VITE_USE_LOCAL_API=true
|
ENV VITE_USE_LOCAL_API=true
|
||||||
|
|
||||||
|
# Dummy Supabase values. Nothing reads Supabase in local mode, but src/lib/supabase.ts
|
||||||
|
# calls createClient() at module scope, and createClient throws "supabaseUrl is
|
||||||
|
# required" on an empty string -- which kills the bundle before React mounts, leaving
|
||||||
|
# a blank page. Its own comment claims it only warns; it does not. A syntactically
|
||||||
|
# valid dummy URL constructs fine and is never called.
|
||||||
|
ENV VITE_SUPABASE_URL=https://noop.invalid
|
||||||
|
ENV VITE_SUPABASE_ANON_KEY=noop
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--max-old-space-size=3072
|
ENV NODE_OPTIONS=--max-old-space-size=3072
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
// ─── Realtime no-op shim (appended to local-api.ts at image build) ───
|
||||||
|
//
|
||||||
|
// The local Flask API has no websocket/realtime layer -- upstream's LOCAL_MODE.md
|
||||||
|
// says so outright ("Realtime subscriptions -- NOT migrated"), but the adapter it
|
||||||
|
// ships still lacks `.channel()`, so every component that opens a subscription
|
||||||
|
// (useSubscription, useCurrentEvent, social, realm multiplayer) throws
|
||||||
|
// "TypeError: supabase.channel is not a function" and trips the ErrorBoundary --
|
||||||
|
// the whole app renders "Something went wrong".
|
||||||
|
//
|
||||||
|
// These stubs satisfy the call shape and do nothing. Reads still work: the app
|
||||||
|
// fetches through the normal query path. What is lost is live push updates, which
|
||||||
|
// are meaningless on a single-user instance with no other clients to sync from.
|
||||||
|
Object.assign(supabase as any, {
|
||||||
|
channel: (_name?: string, _opts?: any) => {
|
||||||
|
const ch: any = {
|
||||||
|
on: () => ch, // chainable, never fires
|
||||||
|
subscribe: (cb?: (status: string) => void) => { cb?.('SUBSCRIBED'); return ch; },
|
||||||
|
unsubscribe: async () => 'ok',
|
||||||
|
presenceState: () => ({}), // realm multiplayer: nobody else here
|
||||||
|
track: async () => 'ok',
|
||||||
|
untrack: async () => 'ok',
|
||||||
|
send: async () => 'ok',
|
||||||
|
};
|
||||||
|
return ch;
|
||||||
|
},
|
||||||
|
removeChannel: async () => 'ok',
|
||||||
|
removeAllChannels: async () => 'ok',
|
||||||
|
getChannels: () => [],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user