Files
Adriano 4de7d78b66 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>
2026-04-26 17:26:43 +02:00

1640 lines
30 KiB
Markdown

# TieMeasureFlow API Reference
## Overview
TieMeasureFlow provides a REST API built with FastAPI for managing measurement tasks, recipes, and statistical analysis. The API uses API Key authentication and supports both single and batch measurement creation.
**Base URL:** `http://localhost:8000/api`
**API Version:** 0.1.0
## Table of Contents
1. [Authentication](#authentication)
2. [Authorization & Roles](#authorization--roles)
3. [Rate Limiting](#rate-limiting)
4. [Response Format](#response-format)
5. [Error Codes](#error-codes)
6. [Endpoints](#endpoints)
- [Auth](#auth)
- [Users](#users)
- [Recipes](#recipes)
- [Tasks & Subtasks](#tasks--subtasks)
- [Measurements](#measurements)
- [Files](#files)
- [Settings](#settings)
- [Stations](#stations)
- [Statistics](#statistics)
- [Reports](#reports)
7. [Pagination](#pagination)
8. [Filtering](#filtering)
## Authentication
### Login Flow
1. **POST `/auth/login`** - Obtain API Key
```json
{
"username": "operator1",
"password": "password123"
}
```
Response:
```json
{
"user": {
"id": 1,
"username": "operator1",
"display_name": "Operator One",
"email": "op1@example.com",
"roles": ["MeasurementTec"],
"is_admin": false,
"active": true,
"language_pref": "it",
"theme_pref": "light"
},
"api_key": "tmf_abc123def456ghi789jkl"
}
```
2. **Include API Key in Subsequent Requests**
```
X-API-Key: tmf_abc123def456ghi789jkl
```
3. **POST `/auth/logout`** - Invalidate API Key
- Requires `X-API-Key` header
- Returns HTTP 204 No Content on success
### Example: Complete Auth Flow
```bash
# Login
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "operator1", "password": "password123"}'
# Response contains api_key: "tmf_..."
# Use API key in subsequent requests
curl -X GET http://localhost:8000/api/auth/me \
-H "X-API-Key: tmf_..."
# Logout
curl -X POST http://localhost:8000/api/auth/logout \
-H "X-API-Key: tmf_..."
```
## Authorization & Roles
All API endpoints require authentication via `X-API-Key` header. Most endpoints require specific roles:
| Role | Permissions |
|------|-------------|
| **Maker** | Create/edit recipes, tasks, subtasks, upload files |
| **MeasurementTec** | Execute measurements, barcode lookup, export CSV |
| **Metrologist** | View SPC statistics, capability indices, control charts |
| **Admin** | Manage users, system settings, regenerate API keys |
Roles are combinable - a user can have multiple roles (stored as JSON array in database).
## Rate Limiting
Rate limits are enforced per IP address using a sliding window algorithm:
| Endpoint | Limit | Window |
|----------|-------|--------|
| `/auth/login` | 5 requests | 60 seconds |
| All other endpoints | 100 requests | 60 seconds |
**Error Response on Rate Limit Exceeded (HTTP 429):**
```json
{
"detail": "Too many login attempts. Please try again later."
}
```
The response includes a `Retry-After` header indicating the number of seconds to wait:
```
Retry-After: 42
```
## Response Format
### Success Response
All successful responses return JSON with appropriate HTTP status codes:
```json
{
"id": 1,
"username": "operator1",
"display_name": "Operator One",
...
}
```
### Error Response
```json
{
"detail": "Invalid username or password"
}
```
## Error Codes
| Code | Meaning | Common Causes |
|------|---------|---------------|
| **400** | Bad Request | Invalid input, validation error, file size exceeded |
| **401** | Unauthorized | Missing/invalid API key, invalid credentials |
| **403** | Forbidden | User lacks required role for operation |
| **404** | Not Found | Resource does not exist |
| **409** | Conflict | Resource conflict (duplicate username, editing non-current recipe version) |
| **422** | Unprocessable Entity | Validation error (invalid role, marker number already exists) |
| **429** | Too Many Requests | Rate limit exceeded |
| **500** | Internal Server Error | Unexpected server error |
## Endpoints
### Health Check
#### GET `/health`
Health check endpoint - no authentication required.
**Response:**
```json
{
"status": "ok",
"service": "TieMeasureFlow API",
"version": "0.1.0"
}
```
---
### Auth
#### POST `/auth/login`
Login with username and password to receive an API key.
**Request:**
```json
{
"username": "string",
"password": "string"
}
```
**Response:** HTTP 200 with `LoginResponse`
```json
{
"user": { ... },
"api_key": "tmf_..."
}
```
**Rate Limit:** 5 requests/min
**Errors:**
- 401: Invalid username or password
- 429: Rate limit exceeded
---
#### GET `/auth/me`
Get current user profile.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200 with `UserResponse`
**Errors:**
- 401: Invalid/missing API key
---
#### PUT `/auth/me`
Update own profile (display_name, language_pref, theme_pref).
**Headers:**
```
X-API-Key: tmf_...
```
**Request:**
```json
{
"display_name": "New Display Name",
"language_pref": "en",
"theme_pref": "dark"
}
```
**Response:** HTTP 200 with updated `UserResponse`
**Errors:**
- 401: Invalid/missing API key
---
#### POST `/auth/logout`
Invalidate the current API key.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 204 No Content
**Errors:**
- 401: Invalid/missing API key
---
### Users
All user management endpoints require **Admin** role.
#### GET `/users`
List all users.
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Response:** HTTP 200 with `list[UserResponse]`
---
#### POST `/users`
Create a new user.
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Request:**
```json
{
"username": "newuser",
"password": "securepass",
"display_name": "New User",
"email": "user@example.com",
"roles": ["MeasurementTec"],
"is_admin": false,
"language_pref": "it",
"theme_pref": "light"
}
```
**Response:** HTTP 201 with `UserResponse`
**Errors:**
- 403: Not admin
- 409: Username already exists
- 422: Invalid role (must be one of: Maker, MeasurementTec, Metrologist)
---
#### PUT `/users/{user_id}`
Update a user.
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Request:** (all fields optional)
```json
{
"display_name": "Updated Name",
"email": "newemail@example.com",
"roles": ["Maker", "MeasurementTec"],
"is_admin": true,
"language_pref": "en"
}
```
**Response:** HTTP 200 with updated `UserResponse`
**Errors:**
- 403: Not admin
- 404: User not found
- 422: Invalid role
---
#### DELETE `/users/{user_id}`
Deactivate a user (soft delete). Sets `active=False` and clears `api_key`.
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Response:** HTTP 204 No Content
**Errors:**
- 400: Cannot deactivate yourself
- 403: Not admin
- 404: User not found
---
#### POST `/users/{user_id}/regenerate-key`
Regenerate API key for a user.
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Response:** HTTP 200
```json
{
"api_key": "tmf_newsecretkey...",
"message": "API key regenerated for user username"
}
```
**Errors:**
- 403: Not admin
- 404: User not found
---
### Recipes
#### POST `/recipes`
Create a new recipe with its initial version (v1).
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:**
```json
{
"code": "RECIPE_001",
"name": "Measurement Recipe 1",
"description": "Optional description",
"change_notes": "Initial version"
}
```
**Response:** HTTP 201 with `RecipeResponse` (includes `current_version`)
**Errors:**
- 403: Maker role required
- 409: Recipe code already exists
---
#### GET `/recipes`
List active recipes with pagination and optional search.
**Headers:**
```
X-API-Key: tmf_...
```
**Query Parameters:**
- `page` (int, default=1, >=1)
- `per_page` (int, default=20, 1-100)
- `search` (string, optional, max_length=200)
**Response:** HTTP 200 with `RecipeListResponse`
```json
{
"items": [...],
"total": 45,
"page": 1,
"per_page": 20,
"pages": 3
}
```
---
#### GET `/recipes/{recipe_id}`
Get recipe detail with current version and all tasks/subtasks.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200 with `RecipeResponse`
**Errors:**
- 404: Recipe not found
---
#### GET `/recipes/code/{code}`
Look up a recipe by barcode/code. **Requires MeasurementTec role.**
**Headers:**
```
X-API-Key: tmf_... (MeasurementTec required)
```
**Response:** HTTP 200 with `RecipeResponse`
**Errors:**
- 403: MeasurementTec role required
- 404: Recipe not found
---
#### PUT `/recipes/{recipe_id}`
Update a recipe. Creates a new version via **copy-on-write**. Existing measurements remain linked to the original version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:**
```json
{
"name": "Updated Name",
"description": "Updated description",
"change_notes": "Updated tolerances"
}
```
**Response:** HTTP 200 with `RecipeResponse` (with new version)
**Errors:**
- 403: Maker role required
- 404: Recipe not found
- 409: Recipe is inactive
---
#### DELETE `/recipes/{recipe_id}`
Deactivate a recipe (soft delete).
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Response:** HTTP 200 with `RecipeResponse`
**Errors:**
- 403: Maker role required
- 404: Recipe not found
---
#### GET `/recipes/{recipe_id}/versions`
List all versions of a recipe.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200 with `list[RecipeVersionResponse]`
---
#### GET `/recipes/{recipe_id}/versions/{version_number}`
Get a specific version with its tasks.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200 with `RecipeVersionResponse`
**Errors:**
- 404: Version not found
---
#### GET `/recipes/{recipe_id}/versions/{version_number}/measurement-count`
Count measurements on a specific version. Useful to warn before creating a new version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Response:** HTTP 200
```json
{
"recipe_id": 1,
"version_number": 1,
"measurement_count": 42
}
```
**Errors:**
- 403: Maker role required
- 404: Recipe or version not found
---
### Tasks & Subtasks
#### GET `/recipes/{recipe_id}/tasks`
List all tasks for the current version of a recipe.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200 with `list[TaskResponse]` (sorted by order_index)
---
#### POST `/recipes/{recipe_id}/tasks`
Add a task to the current version. Creates a **new version via copy-on-write**.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:**
```json
{
"title": "Task Title",
"directive": "Measure diameter at point A",
"description": "Detailed instructions",
"file_type": "image",
"annotations_json": null,
"subtasks": [
{
"marker_number": 1,
"description": "Point A diameter",
"measurement_type": "length",
"nominal": 10.0,
"utl": 10.5,
"uwl": 10.2,
"lwl": 9.8,
"ltl": 9.5,
"unit": "mm"
}
]
}
```
**Response:** HTTP 201 with `TaskResponse`
**Errors:**
- 403: Maker role required
- 404: Recipe not found
- 409: Recipe is inactive
---
#### PUT `/tasks/{task_id}`
Update a task. Must belong to current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:** (all fields optional)
```json
{
"title": "Updated Title",
"directive": "Updated directive"
}
```
**Response:** HTTP 200 with `TaskResponse`
**Errors:**
- 403: Maker role required
- 404: Task not found
- 409: Task belongs to non-current version
---
#### DELETE `/tasks/{task_id}`
Delete a task and its subtasks. Must belong to current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Response:** HTTP 204 No Content
**Errors:**
- 403: Maker role required
- 404: Task not found
- 409: Task belongs to non-current version
---
#### PUT `/tasks/reorder`
Reorder tasks by providing ordered task IDs. All tasks must belong to the same current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:**
```json
{
"task_ids": [3, 1, 2]
}
```
**Response:** HTTP 200 with `list[TaskResponse]` (in new order)
**Errors:**
- 403: Maker role required
- 400: task_ids is empty or tasks from different versions
- 404: One or more task IDs not found
- 409: Tasks belong to non-current version
---
#### POST `/tasks/{task_id}/subtasks`
Add a subtask to a task. Task must belong to current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:**
```json
{
"marker_number": 2,
"description": "Additional measurement point",
"measurement_type": "length",
"nominal": 15.0,
"utl": 15.3,
"uwl": 15.1,
"lwl": 14.9,
"ltl": 14.7,
"unit": "mm"
}
```
**Response:** HTTP 201 with `SubtaskResponse`
**Errors:**
- 403: Maker role required
- 404: Task not found
- 409: Task belongs to non-current version OR marker_number already exists
---
#### PUT `/subtasks/{subtask_id}`
Update a subtask. Its parent task must belong to current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Request:** (all fields optional)
```json
{
"description": "Updated description",
"nominal": 15.5
}
```
**Response:** HTTP 200 with `SubtaskResponse`
**Errors:**
- 403: Maker role required
- 404: Subtask not found
- 409: Parent task belongs to non-current version
---
#### DELETE `/subtasks/{subtask_id}`
Delete a subtask. Its parent task must belong to current version.
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Response:** HTTP 204 No Content
**Errors:**
- 403: Maker role required
- 404: Subtask not found
- 409: Parent task belongs to non-current version
---
### Measurements
#### POST `/measurements/`
Create a single measurement with auto-calculated pass/fail.
**Headers:**
```
X-API-Key: tmf_... (MeasurementTec required)
```
**Request:**
```json
{
"subtask_id": 1,
"version_id": 1,
"value": 10.2,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "usb_caliper"
}
```
**Response:** HTTP 200 with `MeasurementResponse`
```json
{
"id": 1,
"subtask_id": 1,
"version_id": 1,
"measured_by": 2,
"value": 10.2,
"pass_fail": "pass",
"deviation": 0.2,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "usb_caliper",
"measured_at": "2025-02-07T12:34:56",
"synced_to_csv": false
}
```
**Pass/Fail Calculation:**
- `pass`: value is within LWL and UWL
- `warning`: value is within LTL-LWL or UWL-UTL
- `fail`: value is outside LTL or UTL
**Errors:**
- 400: Invalid subtask/version or value out of logical bounds
- 403: MeasurementTec role required
---
#### POST `/measurements/batch`
Create multiple measurements in a batch.
**Headers:**
```
X-API-Key: tmf_... (MeasurementTec required)
```
**Request:**
```json
{
"measurements": [
{
"subtask_id": 1,
"version_id": 1,
"value": 10.2,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "usb_caliper"
},
{
"subtask_id": 2,
"version_id": 1,
"value": 15.1,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "manual"
}
]
}
```
**Response:** HTTP 200 with `list[MeasurementResponse]`
**Errors:**
- 400: Any measurement validation fails (entire batch fails)
- 403: MeasurementTec role required
---
#### GET `/measurements/`
Query measurements with filters and pagination.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, optional)
- `version_id` (int, optional)
- `subtask_id` (int, optional)
- `measured_by` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `pass_fail` (string, optional, enum: pass|warning|fail)
- `page` (int, default=1, >=1)
- `per_page` (int, default=50, 1-500)
**Response:** HTTP 200 with `MeasurementListResponse`
```json
{
"items": [...],
"total": 256,
"page": 1,
"per_page": 50,
"pages": 6
}
```
**Errors:**
- 403: Metrologist role required
---
#### GET `/measurements/export/csv`
Export measurements to CSV with configurable delimiter and decimal separator.
**Headers:**
```
X-API-Key: tmf_... (MeasurementTec or Metrologist required)
```
**Query Parameters:** (same as GET `/measurements/`)
**Response:** HTTP 200 with CSV file (streaming)
**CSV Format:**
```
id,subtask_id,version_id,measured_by,value,pass_fail,deviation,lot_number,serial_number,input_method,measured_at,synced_to_csv
```
Delimiter and decimal separator read from system settings (`csv_delimiter`, `csv_decimal_separator`).
**Errors:**
- 403: Insufficient role
---
### Files
#### POST `/files/upload`
Upload an image or PDF file. **Requires Maker role.**
**Headers:**
```
X-API-Key: tmf_... (Maker required)
Content-Type: multipart/form-data
```
**Form Parameters:**
- `file` (UploadFile, required): Image (JPEG, PNG, GIF, WebP) or PDF
- `recipe_id` (int, optional): Target recipe
- `version_id` (int, optional): Target version
**Constraints:**
- Max file size: 50 MB (configurable via `MAX_UPLOAD_SIZE_MB`)
- Allowed types: JPEG, PNG, GIF, WebP, PDF
- Max filename length: 255 characters
**Response:** HTTP 200
```json
{
"file_path": "1/1/image.jpg",
"thumbnail_path": "1/1/thumbnails/image.jpg",
"file_type": "image/jpeg",
"file_size": 204800
}
```
**Notes:**
- Files stored in `uploads/{recipe_id}/{version_id}/` or `uploads/general/`
- Filenames sanitized (alphanumeric, dots, hyphens, underscores only)
- Thumbnails auto-generated for images (200x200px)
- Duplicate filenames auto-numbered
**Errors:**
- 400: Invalid file type, file size exceeded, invalid filename
- 403: Maker role required
---
#### GET `/files/{file_path}`
Serve a file from the uploads directory.
**Headers:**
```
X-API-Key: tmf_...
```
**Path Parameter:**
- `file_path` (string): Relative path from `uploads/` directory
**Example:**
```
GET /files/1/1/image.jpg
GET /files/1/1/thumbnails/image.jpg
```
**Response:** HTTP 200 with file content (Content-Type inferred)
**Errors:**
- 403: Path traversal attempt
- 404: File not found
---
#### DELETE `/files/{file_path}`
Delete a file from the uploads directory. **Requires Maker role.**
**Headers:**
```
X-API-Key: tmf_... (Maker required)
```
**Notes:**
- Associated thumbnail also deleted if it exists
**Response:** HTTP 204 No Content
**Errors:**
- 403: Maker role required or path traversal attempt
- 404: File not found
---
### Settings
#### GET `/settings/`
Get all system settings as a dictionary.
**Headers:**
```
X-API-Key: tmf_...
```
**Response:** HTTP 200
```json
{
"company_logo_path": "logos/company_logo.png",
"csv_delimiter": ",",
"csv_decimal_separator": "."
}
```
---
#### PUT `/settings/`
Update multiple system settings. **Requires Admin role.**
**Headers:**
```
X-API-Key: tmf_... (Admin required)
```
**Request:**
```json
{
"csv_delimiter": ";",
"csv_decimal_separator": ",",
"custom_setting": "value"
}
```
**Response:** HTTP 200
```json
{
"message": "Updated 3 settings",
"updated_keys": ["csv_delimiter", "csv_decimal_separator", "custom_setting"]
}
```
**Errors:**
- 403: Admin role required
---
#### POST `/settings/logo`
Upload company logo. **Requires Admin role.**
**Headers:**
```
X-API-Key: tmf_... (Admin required)
Content-Type: multipart/form-data
```
**Form Parameters:**
- `file` (UploadFile, required): Image file (JPEG, PNG, GIF, WebP, SVG)
**Response:** HTTP 200
```json
{
"message": "Company logo uploaded successfully",
"logo_path": "logos/company_logo.png",
"file_size": 102400
}
```
**Notes:**
- Stored as `uploads/logos/company_logo.{ext}`
- Updates `company_logo_path` setting in database
**Errors:**
- 400: Invalid file type or size exceeded
- 403: Admin role required
---
### 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
All statistics endpoints **require Metrologist role**.
#### GET `/statistics/summary`
Get pass/fail/warning summary for filtered measurements.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `version_id` (int, optional)
- `subtask_id` (int, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `operator_id` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
**Response:** HTTP 200 with `SummaryData`
```json
{
"total": 100,
"pass_count": 85,
"warning_count": 10,
"fail_count": 5,
"pass_percentage": 85.0,
"warning_percentage": 10.0,
"fail_percentage": 5.0
}
```
---
#### GET `/statistics/capability`
Get capability indices (Cp/Cpk/Pp/Ppk) for a specific subtask.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `subtask_id` (int, required)
- `version_id` (int, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `operator_id` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
**Response:** HTTP 200 with `CapabilityData`
```json
{
"n": 50,
"mean": 10.05,
"stdev": 0.15,
"cp": 1.67,
"cpk": 1.50,
"pp": 1.60,
"ppk": 1.40
}
```
---
#### GET `/statistics/control-chart`
Get control chart data with UCL/LCL and out-of-control detection.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:** (same as capability)
**Response:** HTTP 200 with `ControlChartData`
```json
{
"points": [
{
"x": 0,
"y": 10.1,
"timestamp": "2025-02-01T08:00:00"
},
...
],
"ucl": 10.5,
"lcl": 9.5,
"center_line": 10.0,
"out_of_control_indices": [5, 12, 18]
}
```
---
#### GET `/statistics/histogram`
Get histogram data with normal curve overlay.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `subtask_id` (int, required)
- `version_id` (int, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `operator_id` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
- `n_bins` (int, default=20, 5-100)
**Response:** HTTP 200 with `HistogramData`
```json
{
"bins": [9.0, 9.2, 9.4, 9.6, ...],
"frequencies": [0, 2, 5, 12, ...],
"normal_curve_y": [0.001, 0.003, 0.008, ...]
}
```
---
#### GET `/statistics/subtasks`
Get subtasks for a recipe (for filter dropdown).
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `version_id` (int, optional): Use current version if not provided
**Response:** HTTP 200 with list of subtask objects
```json
[
{
"id": 1,
"marker_number": 1,
"description": "Diameter at point A",
"task_title": "Task 1: Measure",
"nominal": 10.0,
"utl": 10.5,
"ltl": 9.5
},
...
]
```
---
### Reports
All report endpoints **require Metrologist role**.
#### GET `/reports/spc`
Generate and download SPC PDF report with charts and statistics.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `subtask_id` (int, required)
- `version_id` (int, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `operator_id` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
**Response:** HTTP 200 with PDF file (streaming)
**Content-Type:** `application/pdf`
**Filename:** `spc_report_recipe{recipe_id}_st{subtask_id}.pdf`
**Errors:**
- 403: Metrologist role required
- 500: Report generation failed
---
#### GET `/reports/measurements`
Generate and download measurement table PDF report.
**Headers:**
```
X-API-Key: tmf_... (Metrologist required)
```
**Query Parameters:**
- `recipe_id` (int, required)
- `subtask_id` (int, optional)
- `version_id` (int, optional)
- `date_from` (datetime ISO8601, optional)
- `date_to` (datetime ISO8601, optional)
- `operator_id` (int, optional)
- `lot_number` (string, optional)
- `serial_number` (string, optional)
**Response:** HTTP 200 with PDF file (streaming)
**Content-Type:** `application/pdf`
**Filename:** `measurements_report_recipe{recipe_id}.pdf`
**Errors:**
- 403: Metrologist role required
- 500: Report generation failed
---
## Pagination
List endpoints return paginated responses with the following format:
```json
{
"items": [...],
"total": 150,
"page": 1,
"per_page": 20,
"pages": 8
}
```
**Parameters:**
- `page`: Current page number (1-indexed, default=1)
- `per_page`: Items per page (default varies by endpoint, max varies)
**Example:**
```bash
GET /api/recipes?page=2&per_page=50
```
---
## Filtering
Most list endpoints support filtering via query parameters. Common filters:
| Parameter | Type | Example |
|-----------|------|---------|
| `search` | string | `/recipes?search=recipe%20name` |
| `date_from` | ISO8601 datetime | `/measurements?date_from=2025-01-01T00:00:00` |
| `date_to` | ISO8601 datetime | `/measurements?date_to=2025-02-07T23:59:59` |
| `lot_number` | string | `/measurements?lot_number=LOT123` |
| `serial_number` | string | `/measurements?serial_number=SN456` |
| `pass_fail` | string (pass\|warning\|fail) | `/measurements?pass_fail=fail` |
---
## Response Schemas
### UserResponse
```json
{
"id": 1,
"username": "operator1",
"display_name": "Operator One",
"email": "op1@example.com",
"roles": ["MeasurementTec"],
"is_admin": false,
"active": true,
"language_pref": "it",
"theme_pref": "light"
}
```
### RecipeResponse
```json
{
"id": 1,
"code": "RECIPE_001",
"name": "Recipe Name",
"description": "Description",
"active": true,
"created_at": "2025-01-01T10:00:00",
"updated_at": "2025-01-15T14:30:00",
"created_by": 1,
"current_version": { ... }
}
```
### TaskResponse
```json
{
"id": 1,
"version_id": 1,
"order_index": 0,
"title": "Task Title",
"directive": "Measurement directive",
"description": "Detailed description",
"file_type": "image",
"annotations_json": null,
"subtasks": [...]
}
```
### SubtaskResponse
```json
{
"id": 1,
"task_id": 1,
"marker_number": 1,
"description": "Measurement point description",
"measurement_type": "length",
"nominal": 10.0,
"utl": 10.5,
"uwl": 10.2,
"lwl": 9.8,
"ltl": 9.5,
"unit": "mm"
}
```
### MeasurementResponse
```json
{
"id": 1,
"subtask_id": 1,
"version_id": 1,
"measured_by": 2,
"value": 10.15,
"pass_fail": "pass",
"deviation": 0.15,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "usb_caliper",
"measured_at": "2025-02-07T12:34:56",
"synced_to_csv": false
}
```