SPA_AfterTemperRejection/README.md
Erik 7529bc1949 docs: README — data is wired to live MariaDB + reject writes (was "currently SAMPLE")
Overview reads live MariaDB and POST /reject writes since 2026-07-02. Replaced the
"wire it (TODO)" section with the live wiring; noted the HTTP-200+{success:false}
contract and the rejTtId column added by fastpress migration 009; collapsed the
resolved open questions (per-ttId booking + belt semantics), leaving only the
login-free-write security question open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:14:35 +02:00

157 lines
7.3 KiB
Markdown

# 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`.
### Live data wiring
`api/routes.py` reads live MariaDB and `POST /reject` writes (details in the gateway
memory `.claude/memory/ausschuss-station.md`; `sample:true` only in the standalone dev
fallback). Via `from .._db import run_select_query, transaction`:
- **overview:** reads `production.temper_tracking` across **all** presses (un-scoped),
one row per `ttId`, newest-first by `ttOutOfOvenAt` (`ttStatus='done'`), joined to
`active_orders`/`finished_orders`, windowed on `ttOutOfOvenAt`. The order-level
5-station distribution reuses `modules/fastpress/press/temper.py::_temper_aggregates_all`.
- **reasons:** `SELECT id, reason_de, reason_en FROM settings.reject_reasons WHERE active=1`.
- **reject:** one `INSERT` into `production.rejects` inside a `transaction()` (an
un-scoped reuse of PressV `send_reject`), keyed by `rejOrdernumber`+`rejPress` from
the body — the device-scoped `POST /press/rejects/send` can't be reused (it derives
the press from the device JWT). ⚠ `/reject` returns **HTTP 200 + `{success:false}`**
on failure (not 4xx/5xx — else the frontend store swallows it into a fake success).
Note: a nullable `rejTtId` column was later added (fastpress migration 009) so rejects
attribute to the correct press cycle — see the gateway memory
`temper-cycle-reject-attribution.md`.
### Open question before go-live
**Security of the login-free write:** network/reverse-proxy restriction only, or a
static `STATION_TOKEN` in `.env`? (No TLS/reverse-proxy exists yet.) Because this
terminal **writes**, resolve this before exposing it beyond a trusted LAN. (Resolved:
per-`ttId` booking is desired; belt shows the order-level distribution.)
---
## 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
<iframe src="http://<gateway-host>:8800/module/temper_rejects/#/"
width="100%" height="900" style="border:0"></iframe>
```
(`?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.