SPA_AfterTemperRejection/__init__.py
Erik 3fcd9a75cd feat: opt into gateway CORS for cross-origin Grafana iframe embed
Declare CORS_ALLOW_ORIGINS=["*"]. The kernel loader reads this opt-in and emits Access-Control-Allow-Origin for this module's prefix only, so the SPA's Vite `crossorigin` assets load inside a sandboxed Grafana iframe (opaque origin) instead of being CORS-blocked (blank page).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:56:41 +02:00

59 lines
2.7 KiB
Python

"""temper_rejects — the Ausschuss-Station page-module for the SPRO gateway.
A central, **login-free** reject-booking terminal for ALL presses' tempered
carts (Werk Cadolzburg). Mounted into the SPRO Universal Gateway as a git
submodule at ``<gateway>/modules/temper_rejects/`` and discovered by the kernel
module loader (``app.core.modules.load_modules``), which imports this package and
calls ``register(app)``.
It does two things:
* mounts the built Vue SPA (``dist/``) as static files under ``/module/temper_rejects``;
* exposes a small no-auth JSON API under ``/module/temper_rejects/api`` — reads
(tempered carts per ``ttId`` + reject reasons) AND one narrow write
(``POST /reject`` → one row in ``production.rejects``).
No login / device context by design — this is a central shop-floor terminal. The
existing device-scoped ``POST /press/rejects/send`` derives the press from the
device JWT, which is useless for a multi-press terminal; hence the un-scoped
variant here, which takes ``press`` + ``ttId`` from the request body. Non-sensitive
shop-floor data. Security posture (network/reverse-proxy vs. a static token) is an
open question — see this repo's README + ``.claude/memory/ausschuss-station.md``.
See ``agent_docs/vue-page-modules.md`` in the gateway repo for the module runbook.
"""
from __future__ import annotations
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
MODULE_NAME = "temper_rejects"
# MUST match vite.config.ts `base` (with a trailing slash on the Vite side).
MODULE_PREFIX = "/module/temper_rejects"
# CORS opt-in (read by the kernel loader, app.core.modules). This login-free board
# is embedded cross-origin in a Grafana text panel, which renders it inside a
# SANDBOXED iframe → the document gets an opaque origin (Origin: null). Vite tags the
# SPA's own JS/CSS `crossorigin`, so those assets are fetched in CORS mode and the
# browser blocks them (blank page) unless the gateway allows the read. "*" is safe:
# this is no-auth, read-only shop-floor data, and only "*" satisfies Origin: null.
CORS_ALLOW_ORIGINS = ["*"]
def register(app: FastAPI) -> None:
"""Mount the data API and the SPA onto the gateway app.
API routes are registered FIRST — a ``StaticFiles`` mount captures everything
under its prefix, so the ``/api`` routes must exist before the mount or they
get shadowed. ``dist/`` MUST exist (it is committed) or this raises and the
loader logs-and-skips the module (kernel stays up, page 404s).
"""
from .api import routes
app.include_router(routes.router)
dist = Path(__file__).resolve().parent / "dist"
app.mount(MODULE_PREFIX, StaticFiles(directory=dist, html=True), name=MODULE_NAME)