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>
26 KiB
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
- Authentication
- Authorization & Roles
- Rate Limiting
- Response Format
- Error Codes
- Endpoints
- Pagination
- Filtering
Authentication
Login Flow
-
POST
/auth/login- Obtain API Key{ "username": "operator1", "password": "password123" }Response:
{ "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" } -
Include API Key in Subsequent Requests
X-API-Key: tmf_abc123def456ghi789jkl -
POST
/auth/logout- Invalidate API Key- Requires
X-API-Keyheader - Returns HTTP 204 No Content on success
- Requires
Example: Complete Auth Flow
# 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):
{
"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:
{
"id": 1,
"username": "operator1",
"display_name": "Operator One",
...
}
Error Response
{
"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:
{
"status": "ok",
"service": "TieMeasureFlow API",
"version": "0.1.0"
}
Auth
POST /auth/login
Login with username and password to receive an API key.
Request:
{
"username": "string",
"password": "string"
}
Response: HTTP 200 with LoginResponse
{
"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:
{
"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:
{
"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)
{
"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
{
"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:
{
"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
{
"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:
{
"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
{
"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:
{
"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)
{
"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:
{
"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:
{
"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)
{
"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:
{
"subtask_id": 1,
"version_id": 1,
"value": 10.2,
"lot_number": "LOT123",
"serial_number": "SN456",
"input_method": "usb_caliper"
}
Response: HTTP 200 with MeasurementResponse
{
"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 UWLwarning: value is within LTL-LWL or UWL-UTLfail: 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:
{
"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
{
"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 PDFrecipe_id(int, optional): Target recipeversion_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
{
"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}/oruploads/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 fromuploads/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
{
"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:
{
"csv_delimiter": ";",
"csv_decimal_separator": ",",
"custom_setting": "value"
}
Response: HTTP 200
{
"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
{
"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_pathsetting 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
{
"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
{
"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
{
"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
{
"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
[
{
"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:
{
"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:
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
{
"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
{
"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
{
"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
{
"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
{
"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
}