Phase 0: repo skeleton, image, compose stacks, doctor, migration runner, frontend shell

New repo for the Packet Station rebuild (Python 3.12, FastAPI, PostgreSQL 16,
React, VisualKit, MFilesKit, Docker Compose on docker-1). Nothing is migrated
from the M: install.

- CLAUDE.md with the safety rules, README, CHANGELOG, docs (BRIEF, PLAN,
  SECURITY rewritten for the server with the requirement ID table, DEPLOY,
  stubs for FEED, POLICY, SCHEMA, GO-LIVE, PAPER-FALLBACK).
- packetstation CLI: app, worker (heartbeat stub), migrate, doctor, version,
  healthcheck. Settings from the environment with secret files. Stack guard
  (test never points at VISUAL production). Security headers and CSP.
  Trusted proxy resolved by name. JSON logs with redaction.
- Migration runner and 0001_base: settings, append-only audit_events with
  hash chain, ps_notify, grants for ps_app.
- Multi-stage Dockerfile (ODBC Driver 18, PESCO libraries from the Gitea
  index only), compose base, proxy overlay for npm_default, CI overlay,
  postgres initdb roles, env examples, NPM proxy host notes, Gitea Actions.
- Frontend: Vite, React, TypeScript, the PESCO dark theme ported with
  self-hosted fonts, placeholder sign-in and home.
- Verified on docker-1: image builds, CI stack comes up healthy, doctor
  database checks pass, 77 server tests pass against PostgreSQL.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-15 23:08:47 -06:00
co-authored by Claude Fable 5.1
commit 0b663bb2b2
91 changed files with 11307 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Startup guards: the VISUAL target must match STACK, and insecure LDAP is never silent."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
from .settings import Settings
log = logging.getLogger(__name__)
_TARGET_FIELDS = {"server", "database", "profile"}
class StackGuardError(Exception):
"""The configuration must not start."""
@dataclass
class GuardResult:
target: str # e.g. "VSQL\DEV / PESCOINCDEV (DEV)"
production: bool
explicit: bool
warnings: list[str]
def load_visual_settings() -> Any:
"""visualkit's own Settings (VISUAL_* variables). Tests monkeypatch this."""
from visualkit.config import Settings as VisualSettings
return VisualSettings()
def check_stack(settings: Settings, visual: Any | None = None) -> GuardResult:
"""Raise StackGuardError when STACK and the VISUAL target disagree."""
if visual is None:
visual = load_visual_settings()
production = bool(visual.is_production)
explicit = bool(_TARGET_FIELDS & set(getattr(visual, "model_fields_set", set())))
target = f"{visual.server} / {visual.database} ({'PRODUCTION' if production else 'DEV'})"
warnings: list[str] = []
if settings.stack != "prod" and production:
raise StackGuardError(f"STACK={settings.stack} but the VISUAL target is production: {target}")
if settings.stack == "prod" and not production:
if not explicit:
raise StackGuardError(
"STACK=prod but no VISUAL target is set (VisualKit would default to DEV); "
"set VISUAL_PROFILE=prod or VISUAL_SERVER and VISUAL_DATABASE"
)
warnings.append(f"STACK=prod reads a non-production VISUAL target: {target}")
if settings.ldap_allow_insecure:
warnings.append(f"LDAP_ALLOW_INSECURE=true on STACK={settings.stack}: LDAP may run without TLS")
return GuardResult(target, production, explicit, warnings)
def startup_checks(settings: Settings) -> GuardResult:
"""Run at app and worker start: raise on a bad stack, log every warning."""
result = check_stack(settings)
for w in result.warnings:
log.warning(w)
return result