Add self-contained docker/ stack for the web UI

`docker/` now holds the whole web build — frontend (Vite + React + CanvasKit),
backend (Fastify + SQLite) and the compose file — so the folder can be moved to
another machine and run without the React Native project:

    cd docker && cp .env.example .env && docker compose up -d --build

Only `${WEB_PORT:-8090}` is published; nginx serves the SPA and proxies /api to
the `api` container over Docker's DNS. Photos never reach the server.

The shared render code is vendored into `docker/frontend/shared/` and aliased to
a CanvasKit shim, so the app's own frameUtils/toneShader/jpegDpi run unchanged.

Fix the all-black render on GPU surfaces: `MakeWebGLCanvasSurface` creates a
separate WebGL context per call, and a texture from one context cannot be
sampled by a surface on another — so any pass that drew a snapshot onto a second
surface (output sharpen, screen sharpen, polaroid/wallframe cards) came out
solid black, while the raster fallback was correct. Use one shared
GrDirectContext + MakeRenderTarget instead.

Verified in headless Chromium against the running stack: 12MP JPEG in, preview
mean=120.5 sd=60.5, export 2048x1536 mean=107.2 sd=62.1, JFIF density 300/300,
EXIF present, no console errors; health/signup/login/me/recipes all 2xx through
the nginx proxy.
This commit is contained in:
2026-09-17 17:43:03 +07:00
parent 608901bd0f
commit 8c6e7930db
57 changed files with 9359 additions and 2 deletions
@@ -0,0 +1,37 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import { ACCENTS, applyTheme, readAccent, readTheme, type AccentId, type ThemeMode } from './themes';
interface ThemeValue {
mode: ThemeMode;
accent: AccentId;
setMode: (mode: ThemeMode) => void;
setAccent: (accent: AccentId) => void;
}
const Ctx = createContext<ThemeValue | null>(null);
// The tokens live in CSS; this provider only owns the two attributes on <html>
// and the persisted choice. No component ever reads a colour from JS.
export function ThemeProvider({ children }: { children: ReactNode }) {
const [mode, setModeState] = useState<ThemeMode>(readTheme);
const [accent, setAccentState] = useState<AccentId>(readAccent);
useEffect(() => {
applyTheme(mode, accent);
}, [mode, accent]);
const setMode = useCallback((next: ThemeMode) => setModeState(next), []);
const setAccent = useCallback((next: AccentId) => setAccentState(next), []);
const value = useMemo(() => ({ mode, accent, setMode, setAccent }), [mode, accent, setMode, setAccent]);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export function useTheme(): ThemeValue {
const value = useContext(Ctx);
if (!value) throw new Error('useTheme used outside ThemeProvider');
return value;
}
export { ACCENTS };
export type { AccentId, ThemeMode };