# SPA_AfterTemperRejection — Ausschuss-Station A central, **login-free** reject-booking terminal for **all presses' tempered carts** (Werk Cadolzburg). A Vue 3 SPA that the **SPRO Universal Gateway** serves as a static page-module under `/module/temper_rejects/`. It lists every tempered cart (one row per `ttId`, newest first), shows each order's temper flow, and lets a shop-floor operator book an Ausschuss (count + reason) against a specific cart. > **Standalone orientation.** Developed on its own (sibling repo, like `FastPress` / > `PressV`), mounted into the gateway as a submodule at `modules/temper_rejects/`. > This README bakes in everything a fresh session needs. Design is **LOCKED** (see `.claude/memory/ausschuss-station.md` in the gateway repo): per-`ttId` list · press checkbox filter on the left (default **all**, greyed when a press has no carts in the window, hit-count badge) · **style-C** 5-station temper belt under each entry (number badges + animated conveyor) · time window 12h/1T/7T/14T/30T. Touch-density controls (56px). Run `npm run dev` to see it. --- ## Quick start (standalone dev) Package manager is **yarn 1.22.22** (pinned via `packageManager`; matches PressV). `corepack` provides it. ```bash corepack enable # one-time per machine (honours the pinned yarn) cp .npmrc.example .npmrc # paste a Gitea PAT with "package: Read" scope (yarn v1 reads .npmrc) yarn install yarn dev # http://localhost:5173/module/temper_rejects/ ``` With **no gateway backend**, the store falls back to **bundled sample data** (`src/data/sample.ts`) and the booking flow simulates success — so the whole terminal (filter, belt, dialog) is demoable offline (a `DEMO-DATEN` tag shows). `yarn build` → `dist/` (committed). --- ## How it's wired into the gateway (page-module contract) Per `agent_docs/vue-page-modules.md`. The **repo root is a Python package** (`__init__.py`) so the loader can `import temper_rejects` and call `register(app)`: | File | Role | |---|---| | `__init__.py` | `register(app)` — mounts `dist/` at `MODULE_PREFIX` + includes the API router | | `_db.py` | kernel DB seam — `run_select_query` (reads) **and** `transaction()` (the reject write) | | `api/routes.py` | no-auth API: `GET /overview`, `GET /reasons`, `POST /reject` | | `dist/` | **committed** build output — the gateway serves this | **URL prefix in two places that MUST match:** `vite.config.ts` → `base: '/module/temper_rejects/'` and `__init__.py` → `MODULE_PREFIX = "/module/temper_rejects"`. The frontend derives its API base from `import.meta.env.BASE_URL`. ```bash # in the gateway repo (fastAPI) git submodule add https://git.sprodat.eu/Erik/SPA_AfterTemperRejection.git modules/temper_rejects # MODULES_ENABLED must include temper_rejects (or be empty = load all) ``` Restart → log shows `loaded module: temper_rejects`. Smoke: `http://localhost:8800/module/temper_rejects/` and `…/api/overview`. **Deploy:** `yarn build`, commit `dist/`, bump the submodule pin. Never build on the host. --- ## Data API contract Types are authoritative in [`src/types/station.ts`](src/types/station.ts); Python mirrors them in [`api/routes.py`](api/routes.py). - `GET /module/temper_rejects/api/overview?window=12h|1T|7T|14T|30T` → tempered carts (per `ttId`, newest first) + the press list for the filter. - `GET /module/temper_rejects/api/reasons` → active reject reasons (`id`, `reason_de`, `reason_en`). - `POST /module/temper_rejects/api/reject` → body `{ press, orderNo, ttId, count, reason }`. ```jsonc // overview { "window":"7T","generatedAt":"…","sample":true, "presses":["P1","P2","P3","P4","P5","P6","P7","P8","P9"], "entries":[{ "ttId":10231,"cartId":"00A17226","press":"P5","orderNo":"O-4455", "article":"Gehäuse 12 mm","partsGood":60,"scrap":2,"doneAt":"…", "flow":{ "notYetSent":120,"wagen":60,"unterwegs":40,"ofen":72,"fertig":260 } }] } ``` The `flow` object is the **order-level** 5-station distribution shown on the belt. Belt colour mapping (matches PressV process colours): `notYetSent`→violet, `wagen`→cyan, `unterwegs`→amber, `ofen`→orange, `fertig`→emerald; the reject accent is Grafana dark-red `#C4162A`. ### Wiring the REAL data (currently SAMPLE) `api/routes.py` returns sample data and `POST /reject` echoes success **without writing**. To make it live (see the `TODO` there + `.claude/memory/ausschuss-station.md`): - `from .._db import run_select_query, transaction`. - **overview:** read `production.temper_tracking` across **all** presses (un-scoped), one row per `ttId`, newest first by `ttOutOfOvenAt` (`ttStatus='done'`); join `active_orders`/`finished_orders` for the article. Add each order's 5-station distribution — reuse `modules/fastpress/press/temper.py::_temper_aggregates_all` (notYetSent / sentToTemper / onWayToOven / inOven / finishedTempering). Filter by `window` on `ttOutOfOvenAt`. - **reasons:** `SELECT id, reason_de, reason_en FROM settings.reject_reasons WHERE active=1`. - **reject:** one `INSERT` into `production.rejects` inside a `transaction()` — same shape as `modules/fastpress/press/rejects.py::send_reject`. Booking references the cart, which needs an **additive nullable `rejTtId` column** (a DBA applies it; the runtime account has no `ALTER`). The device-scoped `POST /press/rejects/send` can't be reused — it derives the press from the device JWT; here `press`+`ttId` come from the body. ### Open questions before go-live (from the design memory) 1. **Security of the login-free write:** network/reverse-proxy restriction only, or a static `STATION_TOKEN` in `.env`? (No TLS/reverse-proxy exists yet.) 2. Confirm the `rejTtId` column name + that per-`ttId` booking is desired (yes, per spec). 3. Belt semantics: order-level distribution (chosen) vs. the cart's own journey position. --- ## Tech stack (matches PressV / the fleet) Vue 3.5 · Vite 8 · TS 5.9 · Pinia 3 · vue-router 4 (hash) · vue-i18n 10 (de default, `?lang=en`) · PrimeVue 4.5 (Nora + **Industrial** preset — the booking dialog uses `Dialog`/`Select`/`InputNumber`/`Button`) · Tailwind v4 · `@sp-ui-kit/tokens` · Inter Variable. **Dark-first**, `data-density="touch"` (56px controls), flat 2px corners, reduced-motion disables the conveyor. Store: `src/stores/temperRejects.ts` — `hydrate()` (overview + reasons) → `refresh()` → `startPolling(20000)`; press filter state (`checkedPresses`, default all) + `submitReject()` (posts, then refreshes; simulates success with no backend). Falls back to sample data when a response lacks an `entries[]` array. > Not used yet but available in the fleet: `@sp-ui-kit/ui-core` (e.g. `SpNumpadDialog` > would be an even more touch-friendly count entry than the PrimeVue `InputNumber`). ### Design tokens Flow/reject accents (`src/style.css`): nicht-gesendet `#a78bfa`, Wagen `#22d3ee`, Unterwegs `#fbbf24`, Ofen `#f97316` (hot `#ef4444`), Fertig `#34d399`, Ausschuss `#C4162A` — `:root` dark + `html:not(.dark)` light variants. --- ## Grafana embedding ```html ``` (`?lang=en` before the `#` for English.) Gateway sets no frame headers, so it embeds directly. Because this terminal **writes**, resolve open question #1 (security) before exposing it beyond a trusted LAN.