dd2ebf863a
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>
1456 lines
26 KiB
Markdown
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
|
|
}
|
|
```
|