docs: document stations end-to-end (user guide + API + deployment)

Stations were the headline V2.0.0 feature but had no user-facing
documentation outside the architecture page. Filled the gap across
the three operational docs.

USER_GUIDE.md
- New entries in "Key Concepts": Station and Station assignment.
- New "Recipes you see are filtered by station" subsection in the
  MeasurementTec workflow, explaining why the Select Recipe page may
  legitimately show fewer recipes than expected and what the
  "Stazione non configurata" error means at the operator level.
- New "Station Management" section under Admin Workflow covering:
  the mental model, station create/edit/delete, the two-column
  recipe-assignment modal, the immutable-code rule, the role of the
  ST-DEFAULT seed station, and the tablet deployment cheat sheet.
- Admin role description updated to mention stations.

DEPLOYMENT.md
- Environment Variables Reference: added STATION_CODE row and noted
  that an empty value triggers the deliberate fail-fast HTTP 503 on
  /measure/select. Updated RATE_LIMIT_GENERAL default (300, per the
  V2.0.0 perf change). Clarified UPLOAD_DIR resolves against the
  project root.

API.md
- New "Stations" endpoint section listing all eight routes with
  request/response examples and the 401/403/404/409 error contract:
  GET / POST /stations, GET /stations/{id}, PUT /stations/{id},
  DELETE /stations/{id}, GET /stations/{id}/recipes,
  GET /stations/by-code/{code}/recipes (the operator-facing one used
  by the Flask client), POST /stations/{id}/recipes,
  DELETE /stations/{id}/recipes/{recipe_id}.
- TOC updated with the new "Stations" anchor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:26:43 +02:00
parent 2a2d40bec9
commit 4de7d78b66
3 changed files with 277 additions and 6 deletions
+184
View File
@@ -23,6 +23,7 @@ TieMeasureFlow provides a REST API built with FastAPI for managing measurement t
- [Measurements](#measurements) - [Measurements](#measurements)
- [Files](#files) - [Files](#files)
- [Settings](#settings) - [Settings](#settings)
- [Stations](#stations)
- [Statistics](#statistics) - [Statistics](#statistics)
- [Reports](#reports) - [Reports](#reports)
7. [Pagination](#pagination) 7. [Pagination](#pagination)
@@ -1101,6 +1102,189 @@ Content-Type: multipart/form-data
--- ---
### Stations
Stations model the physical measurement posts on the shop floor. Each Flask client identifies itself through `STATION_CODE`, and the operator only sees recipes assigned to that station. See the user guide section "Admin Workflow → Station Management" for the operational model.
#### GET `/stations`
List stations. **Admin only.**
Query parameters:
- `active_only` (bool, default `false`): if `true`, return only stations where `active = true`.
Response `200`:
```json
[
{
"id": 1,
"code": "ST-DEFAULT",
"name": "Default Station",
"location": "Initial seed - change me",
"notes": null,
"active": true,
"created_by": 5,
"created_at": "2026-04-26T10:12:06"
}
]
```
Errors:
- 401: Missing or invalid API key
- 403: Admin role required
---
#### POST `/stations`
Create a station. **Admin only.**
Request body:
```json
{
"code": "ST-LINEA-A",
"name": "Linea A — Tornitura alberi",
"location": "Reparto 2 — Cella 3",
"notes": "Provisioned 2026-04-26 by Adriano",
"active": true
}
```
`code` (1-100 chars) and `name` (1-255 chars) are required. `location` is optional (≤ 255 chars). `active` defaults to `true`.
Response `201`: same shape as `GET /stations` items.
Errors:
- 400: Invalid payload (missing `code`/`name`, exceeded length)
- 403: Admin role required
- 409: Station with that code already exists
---
#### GET `/stations/{station_id}`
Get a single station by id. **Admin only.**
Response `200`: same as the list item shape.
Errors:
- 403: Admin role required
- 404: Station not found
---
#### PUT `/stations/{station_id}`
Update a station's editable fields. **Admin only.**
Request body (all fields optional):
```json
{
"name": "Linea A — riconfigurata",
"location": "Reparto 2 — Cella 4",
"notes": "Moved cell on 2026-05-12",
"active": false
}
```
Note: the `code` field is not in the schema. Codes are immutable on purpose (changing the code would orphan every tablet pointing at it).
Response `200`: the updated station.
Errors:
- 400: Invalid payload
- 403: Admin role required
- 404: Station not found
---
#### DELETE `/stations/{station_id}`
Delete a station. **Admin only.**
The deletion cascades to every row in `station_recipe_assignments` for that station. Existing measurements are NOT affected (measurements link to recipe versions, not stations).
Response `204`: no body.
Errors:
- 403: Admin role required
- 404: Station not found
---
#### GET `/stations/{station_id}/recipes`
Admin view of the recipes currently assigned to a station. Returns the same projection used by the assignment modal in `/admin/stations`. **Admin only.**
Response `200`:
```json
[
{
"id": 2,
"code": "DEMO-001",
"name": "Demo Measurement Recipe",
"active": true
}
]
```
Errors:
- 403: Admin role required
- 404: Station not found
---
#### GET `/stations/by-code/{code}/recipes`
**Operator view** (any authenticated user, no admin requirement). Returns the active recipes assigned to the station whose code matches `{code}`. The Flask client calls this endpoint at every page load of `/measure/select`, passing its own `STATION_CODE`.
Response `200`: same shape as `GET /stations/{station_id}/recipes`.
Errors:
- 401: Missing or invalid API key
- 404: Station not found OR station is not active (operator-facing endpoint deliberately treats both cases as 404 to avoid leaking the existence of disabled stations)
---
#### POST `/stations/{station_id}/recipes`
Assign an existing recipe to a station. **Admin only.**
Request body:
```json
{ "recipe_id": 2 }
```
Response `201`:
```json
{
"id": 1,
"station_id": 1,
"recipe_id": 2,
"assigned_by": 5,
"assigned_at": "2026-04-26T10:12:06"
}
```
Errors:
- 403: Admin role required
- 404: Station or recipe not found
- 409: Recipe already assigned to this station
---
#### DELETE `/stations/{station_id}/recipes/{recipe_id}`
Remove an existing assignment. **Admin only.**
Response `204`: no body.
Errors:
- 403: Admin role required
- 404: Station/recipe not found, or no assignment between them
---
### Statistics ### Statistics
All statistics endpoints **require Metrologist role**. All statistics endpoints **require Metrologist role**.
+4 -3
View File
@@ -134,10 +134,11 @@ SSL_KEYFILE=
| `CLIENT_HOST` | string | 0.0.0.0 | Flask client bind address | | `CLIENT_HOST` | string | 0.0.0.0 | Flask client bind address |
| `CLIENT_PORT` | int | 5000 | Flask client port | | `CLIENT_PORT` | int | 5000 | Flask client port |
| `SERVER_CORS_ORIGINS` | string | http://localhost:5000 | Comma-separated CORS origins | | `SERVER_CORS_ORIGINS` | string | http://localhost:5000 | Comma-separated CORS origins |
| `UPLOAD_DIR` | string | uploads | Directory for file uploads | | `UPLOAD_DIR` | string | uploads | Directory for file uploads (resolved against the project root) |
| `MAX_UPLOAD_SIZE_MB` | int | 50 | Maximum upload file size in MB | | `MAX_UPLOAD_SIZE_MB` | int | 50 | Maximum upload file size in MB |
| `RATE_LIMIT_LOGIN` | int | 5 | Login requests per minute | | `RATE_LIMIT_LOGIN` | int | 5 | Login requests per minute, per real client IP |
| `RATE_LIMIT_GENERAL` | int | 100 | General requests per minute | | `RATE_LIMIT_GENERAL` | int | 300 | General requests per minute, per real client IP (post-V2.0.0; was 100 in V1.0.x) |
| `STATION_CODE` | string | (empty) | **Per-tablet** code identifying the station this Flask client serves. Must match a station created in the admin UI. Empty = the client refuses `/measure/select` with HTTP 503 "Stazione non configurata". |
| `SSL_CERTFILE` | string | (empty) | Path to SSL certificate (production) | | `SSL_CERTFILE` | string | (empty) | Path to SSL certificate (production) |
| `SSL_KEYFILE` | string | (empty) | Path to SSL private key (production) | | `SSL_KEYFILE` | string | (empty) | Path to SSL private key (production) |
+89 -3
View File
@@ -37,6 +37,8 @@ TieMeasureFlow is a web-based measurement management system that enables teams t
| **Subtask** | Individual measurement point with tolerance limits (UTL/UWL/LWL/LTL) | | **Subtask** | Individual measurement point with tolerance limits (UTL/UWL/LWL/LTL) |
| **Measurement** | Individual recorded value with automatic pass/fail/warning status | | **Measurement** | Individual recorded value with automatic pass/fail/warning status |
| **Lot/Serial** | Traceability fields linking measurements to physical parts | | **Lot/Serial** | Traceability fields linking measurements to physical parts |
| **Station** | A physical measurement post (typically one tablet on a production line). Each station has a unique code (`STATION_CODE`) and a list of assigned recipes |
| **Station assignment** | Many-to-many link between a station and the recipes available to the operators using that station's tablet |
### Architecture ### Architecture
@@ -175,23 +177,25 @@ TieMeasureFlow has four primary roles. Users can have multiple roles simultaneou
### Admin ### Admin
**Purpose:** System administration and user management **Purpose:** System administration, user management and station deployment
**Permissions:** **Permissions:**
- Create/edit/delete users - Create/edit/delete users
- Assign roles to users - Assign roles to users
- Regenerate user API keys - Regenerate user API keys
- Create/edit/delete stations and assign recipes to them
- Configure system settings - Configure system settings
- Upload company logo - Upload company logo
- Manage CSV export settings - Manage CSV export settings
**Access:** **Access:**
- Menu: **Admin** → User Management - Menu: **Admin** → Utenti / Stazioni
- Can see: All users and system settings - Can see: All users, all stations and system settings
**Typical Tasks:** **Typical Tasks:**
- Onboard new users - Onboard new users
- Reset lost API keys - Reset lost API keys
- Roll out a new tablet: create the matching station, assign recipes, hand the `STATION_CODE` to devops
- Configure locale/format settings - Configure locale/format settings
- Upload company branding - Upload company branding
- Manage system access - Manage system access
@@ -276,6 +280,12 @@ When you edit a recipe (add/remove tasks, change tolerances), a **new version**
## MeasurementTec Workflow ## MeasurementTec Workflow
### Recipes you see are filtered by station
Each tablet/PC running the Flask client is configured at deployment time with a `STATION_CODE` environment variable (for example `ST-LINEA-A`). Whenever you open **Select Recipe**, the page only lists recipes that the admin has assigned to that station. If your tablet shows fewer recipes than you expect, ask the admin to assign the missing recipes to your station — see **Admin Workflow → Station Management**.
If the page shows "Stazione non configurata" (HTTP 503), the deployment is missing the `STATION_CODE` setting; this is a deploy-time configuration issue, not something the operator can fix from the UI.
### Select a Recipe ### Select a Recipe
**Method 1: Search** **Method 1: Search**
@@ -503,6 +513,82 @@ If user loses their API key:
4. New key is generated and displayed 4. New key is generated and displayed
5. Provide new key to user (display once only) 5. Provide new key to user (display once only)
### Station Management
Stations are how TieMeasureFlow enforces "this tablet is responsible for these measurements". Each physical measurement post in the shop floor is modelled as a station; each tablet's Flask client identifies itself through a `STATION_CODE` env var that must match a station's code in the database.
The page is reachable from the navbar entry **"Stazioni"** (workstation icon) for any user with the admin flag, or directly at `/admin/stations`.
#### Mental model
- **One station = one tablet/PC** in the shop floor. The station's identity (`STATION_CODE`) is configured **once at deploy time** in the tablet's `.env`, never changed at runtime.
- A station has a list of **assigned recipes**. The operator using that tablet sees exactly that list — no more, no less.
- A recipe can be assigned to several stations (e.g. a calibration recipe everyone needs).
- A station can be temporarily **disabled** (`active = false`) to take a line offline without losing its history; tablets pointing at a disabled station get HTTP 404 from the recipe endpoint.
#### Create a Station
1. Navigate to **Admin****Stazioni**
2. Click **Nuova Stazione**
3. Fill in the modal:
- **Codice** (required, unique): `ST-LINEA-A`. This is the value the tablet will set as `STATION_CODE` in its `.env`. Use ASCII letters, digits and hyphens only — no spaces, no accented characters. Once created the code cannot be changed (changing it would break every tablet pointing at it).
- **Nome** (required): human-readable description, e.g. `Linea A — Tornitura alberi`.
- **Postazione** (optional): physical location, e.g. `Reparto 2 — Cella 3`.
- **Note** (optional): free text, useful for tracking who set up the station.
- **Attiva** (default checked): uncheck only when retiring the station.
4. Click **Crea Stazione**
Naming convention: prefix every station code with `ST-` and use a stable identifier that survives shop-floor reorganisations.
#### Assign Recipes to a Station
1. From the stations table, click the **checklist icon** on the row of the target station
2. The "Ricette Assegnate" modal opens with two columns:
- **Ricette disponibili** (left): every recipe in the system not yet assigned to this station. Each row has an inline **+ Assegna** button that immediately moves the recipe to the right column.
- **Assegnate alla stazione** (right): the list the operator at this station's tablet will see. The **X** button removes the assignment.
3. Use the search field at the top to narrow either column by recipe code or name
4. Empty-state hints tell you why a column is empty:
- "Tutte le ricette sono già assegnate" — nothing more to assign
- "Nessuna ricetta nel sistema" — create at least one recipe first (Maker workflow)
- "Nessun risultato per il filtro" — clear the search
The assignment audit trail (`assigned_by`, `assigned_at`) is stored on the server but not currently shown in the UI.
#### Edit a Station
1. Click the **pencil icon** (or the row itself) for the target station
2. Update **Nome**, **Postazione**, **Note** or the **Attiva** flag
3. Click **Salva Modifiche**
The **Codice** field is read-only on edit because the value is contractually bound to the `STATION_CODE` set on already-deployed tablets. To rename a station you must delete it and create a new one with the new code, then update every affected tablet's `.env`.
#### Delete a Station
1. Click the **trash icon** for the station
2. Confirm in the modal
Deletion cascades to **all recipe assignments** for that station. Existing measurements collected by tablets at that station are not affected (measurements are linked to recipe versions, not stations).
#### The `ST-DEFAULT` station
The first time `/api/setup/seed` runs, it creates a station called `ST-DEFAULT` and assigns every demo recipe to it. This is intended for single-tablet demos and dev setups where no per-station segmentation is needed.
In multi-station production deployments you should either:
- Delete `ST-DEFAULT` once your real stations are configured, or
- Disable it (`Attiva` off), to prevent a misconfigured tablet from accidentally inheriting the default assignment set.
#### Tablet deployment cheat sheet
For each new physical tablet:
1. Admin creates the station in the UI (e.g. `ST-LINEA-A`)
2. Admin assigns the relevant recipes
3. Devops sets `STATION_CODE=ST-LINEA-A` in the tablet's `.env`
4. Tablet container starts; the operator opens **Measure → Select Recipe** and sees the curated list
If step 3 is missed, **Select Recipe** shows the page "Stazione non configurata" — this is the intentional fail-fast behaviour to prevent a tablet from silently falling back to the wrong recipe set.
### System Settings ### System Settings
#### Configure CSV Export #### Configure CSV Export