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
)