SPA_AfterTemperRejection/README.md
Erik 27edd2664a chore: use yarn (fleet standard) + declare vite devDependency
- Switch package manager from npm to yarn 1.22.22 (pinned via packageManager,
  matches PressV/sp-ui-kit): remove package-lock.json, add yarn.lock (v1).
- Add "vite" as an explicit devDependency. npm 7+ auto-installed it as a peer of
  @vitejs/plugin-vue / @tailwindcss/vite, which masked the missing declaration; a
  clean `yarn install` (no peer auto-install) needs it explicit or the tsc build
  fails to resolve 'vite' / 'vite/client'.
- README: npm -> yarn (+ `corepack enable`).

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

156 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`.
### 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
<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.