Scoped DB role + own theme colour (port log E17)

- Split database.py into a runtime engine (scoped `kds` DB role, used for
  all request handling) and a migration engine (privileged `kitchen` role,
  used only at startup to create KDS's own tables and ALTER kitchen_settings)
  — KDS previously shared kitchen's full-access DB credential wholesale
- Dispose both engines on shutdown (main.py)
- Fix theme colour clash: KDS was accidentally seeded with kitchen's teal
  (#0d9488) instead of its own colour — now #ea580c (orange), regenerated
  PWA icons to match

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-06 14:45:01 +00:00
parent 870027faca
commit 23889315f5
7 changed files with 4446 additions and 47 deletions

View file

@ -2,25 +2,48 @@ import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
DATABASE_URL = os.getenv(
def _to_asyncpg(url: str) -> str:
if url.startswith("postgresql://"):
return url.replace("postgresql://", "postgresql+asyncpg://", 1)
return url
# Runtime connection — the scoped `kds` DB role, used for every request via
# get_db(). Only has SELECT/INSERT/UPDATE/DELETE on kds_tickets,
# kds_course_bumps and the specific kitchen_settings columns KDS needs (see
# deploy_kds grants) — it cannot read the rest of kitchen_db.
DATABASE_URL = _to_asyncpg(os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://kitchen:kitchen_secret@localhost:5432/kitchen_gp"
"postgresql://kitchen:kitchen_secret@localhost:5432/kitchen_gp"
))
# Migration connection — the privileged `kitchen` DB role. Used ONLY at
# startup to create KDS's own tables and ALTER kitchen_settings (adding KDS
# columns). Never used for request handling. Falls back to DATABASE_URL for
# local/dev convenience, but production must set this separately once the
# `kds` role is scoped down (see port log E17/task: KDS DB role split).
MIGRATION_DATABASE_URL = _to_asyncpg(
os.getenv("MIGRATION_DATABASE_URL") or os.getenv(
"DATABASE_URL", "postgresql://kitchen:kitchen_secret@localhost:5432/kitchen_gp"
)
)
# Convert standard postgres URL to asyncpg format
if DATABASE_URL.startswith("postgresql://"):
DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://", 1)
# `engine` stays the name existing migration files (`from database import
# engine`) already import — no changes needed there. It's bound to the
# privileged connection.
engine = create_async_engine(MIGRATION_DATABASE_URL, echo=False)
engine = create_async_engine(
runtime_engine = create_async_engine(
DATABASE_URL,
echo=False,
pool_size=10, # Default is 5
max_overflow=20, # Default is 10 - allows burst to 30 connections
pool_pre_ping=True # Verify connections are alive before use
pool_pre_ping=True # Verify connections are alive before use
)
AsyncSessionLocal = async_sessionmaker(
engine,
runtime_engine,
class_=AsyncSession,
expire_on_commit=False
)

View file

@ -5,7 +5,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from database import engine, Base
from database import engine, runtime_engine, Base
logging.basicConfig(
level=logging.INFO,
@ -34,7 +34,9 @@ async def _run(name, coro):
@asynccontextmanager
async def lifespan(app: FastAPI):
# Create any SQLAlchemy-mapped KDS tables (idempotent)
# Create any SQLAlchemy-mapped KDS tables (idempotent). Uses the
# privileged migration engine (`kitchen` role) — the scoped `kds` role
# used for request handling can't CREATE TABLE.
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@ -51,6 +53,7 @@ async def lifespan(app: FastAPI):
await stop_signalr_listener()
await engine.dispose()
await runtime_engine.dispose()
app = FastAPI(

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

View file

@ -20,7 +20,7 @@
--kds-sent: #6b7280;
/* App primary — teal (shared with kitchen for recipe images etc.) */
--app-primary: #0d9488;
--app-primary: #ea580c; /* kds orange — distinct from kitchen's teal, D5 */
}
*, *::before, *::after {

View file

@ -14,8 +14,8 @@ export default defineConfig({
start_url: '/kds/',
scope: '/kds/',
display: 'standalone',
theme_color: '#0d9488',
background_color: '#0d9488',
theme_color: '#ea580c',
background_color: '#ea580c',
icons: [
{ src: '/kds/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/kds/icons/icon-512.png', sizes: '512x512', type: 'image/png' },