Files
TieMeasureFlow/docs/API.md
Adriano dd2ebf863a feat: FASE 7 - Polish & Testing (security, i18n, test suite, docs)
Security hardening: CORS lockdown, rate limiting middleware con sliding
window e eviction IP stale, security headers (CSP, HSTS, X-Frame-Options),
session cookie hardening, filename sanitization upload.

i18n completion: internazionalizzati barcode.js e csv-export.js con bridge
window.BARCODE_I18N/CSV_I18N, aggiornati .po IT/EN con 27 nuove stringhe.

Tablet UX: touch target 44px per dispositivi coarse pointer.

Test suite: 101 test totali (76 server + 25 client), copertura completa
di tutti i router API, autenticazione, ruoli, CRUD, SPC, file upload,
security integration. Infrastruttura SQLite async in-memory con fixtures.

Fix critici: MissingGreenlet in recipe_service (selectinload eager),
route ordering tasks.py, auth_service bcrypt diretto, Measurement.id
Integer per SQLite.

Documentazione: API.md (riferimento completo 40+ endpoint),
DEPLOYMENT.md (guida produzione con Docker/Nginx/SSL),
USER_GUIDE.md (manuale utente per ruolo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:10:24 +01:00

1456 lines
26 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)
- [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
---
### 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
}
```