Initial KDS scaffold — Phase 2 kitchen port

FastAPI backend (Python 3.11, httpx for SignalR/GraphQL — no MSSQL ODBC),
shares kitchen_db directly. React/TS/Vite fullscreen board frontend.

Backend: auth.py (APP_SLUG=kds, SimpleNamespace), main.py (4 KDS migrations,
SignalR start/stop), kds.py router, models (kds/settings/resos — read from
kitchen_db), signalr_listener.py (backoff pre-existing), kds_graphql.py,
database.py. Requirements stripped to ~9 packages; image ~400 MB lighter
than kitchen (no MSSQL ODBC layer).

Frontend: AuthGate (app=kds), single fullscreen route, dark board theme.
KDS.tsx URL prefix patched (/api/kds/ → /kds/api/kds/),
recipe images cross-app (/kitchen/api/recipes/).
nginx: 5 blocks with SSE proxy headers on /kds/api/ block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 12:15:16 +00:00
commit b94585084a
35 changed files with 5195 additions and 0 deletions

View file

@ -0,0 +1,67 @@
import { createContext, useContext, useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import type { User } from '../types'
interface AuthCtx {
user: User
token: string
restrictedPages: string[]
login: (t: string) => void
logout: () => void
}
const Ctx = createContext<AuthCtx | null>(null)
export function useAuth() {
const ctx = useContext(Ctx)
if (!ctx) throw new Error('useAuth outside AuthGate')
return ctx
}
export default function AuthGate({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [checking, setChecking] = useState(true)
useEffect(() => {
fetch('/kds/api/auth/verify?app=kds', { credentials: 'include' })
.then((r) => {
if (!r.ok) throw new Error('unauth')
return r.json()
})
.then((data) =>
setUser({
email: data.email || data.sub || '',
name: data.name || data.display_name || '',
is_admin: data.is_admin ?? false,
caps: data.caps ?? [],
})
)
.catch(() => {
;(window.top ?? window).location.href = '/login'
})
.finally(() => setChecking(false))
}, [])
if (checking) {
return (
<div style={{
position: 'fixed', inset: 0,
background: 'var(--kds-bg)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div className="spinner" style={{ width: 32, height: 32 }} />
</div>
)
}
if (!user) return null
const ctx: AuthCtx = {
user,
token: '__session__',
restrictedPages: [],
login: () => {},
logout: () => { (window.top ?? window).location.href = '/login' },
}
return <Ctx.Provider value={ctx}>{children}</Ctx.Provider>
}