# 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
}
```
