openapi.yaml
Rama main · réplica de lectura del repositorio original.
openapi: 3.0.3
info:
title: Meistertracker API
version: 1.0.0
description: REST API for the Meistertracker mushroom lab management system.
servers:
- url: https://localhost:3000
description: Local development (HTTPS)
- url: http://localhost:3000
description: Local development (HTTP fallback)
components:
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: session
schemas:
Error:
type: object
properties:
error:
type: string
Ok:
type: object
properties:
ok:
type: boolean
MushroomStrain:
type: object
required: [id, name, kuerzel, created]
properties:
id:
type: integer
description: Primary key
name:
type: string
description: Full strain name (unique)
kuerzel:
type: string
description: Short code (unique)
description:
type: string
created:
type: string
format: date-time
updated:
type: string
format: date-time
nullable: true
Stamm:
type: object
required: [id, strainId, name, created]
properties:
id:
type: integer
strainId:
type: integer
description: The Pilzsorte it belongs to
name:
type: string
description: Unique per Pilzsorte, case-insensitive
notes:
type: string
created:
type: string
format: date-time
updated:
type: string
format: date-time
nullable: true
security:
- cookieAuth: []
paths:
/api/auth/setup-required:
get:
summary: Check if initial setup is needed
security: []
responses:
'200':
description: Setup status
content:
application/json:
schema:
type: object
properties:
setupRequired:
type: boolean
/api/auth/setup:
post:
summary: Initial admin account setup
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username:
type: string
password:
type: string
minLength: 8
responses:
'200':
description: Setup complete
'400':
description: Validation error
/api/auth/login:
post:
summary: User login
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username:
type: string
password:
type: string
responses:
'200':
description: Login successful, session cookie set
'401':
description: Invalid credentials
/api/auth/logout:
post:
summary: User logout
responses:
'200':
description: Session invalidated
/api/auth/me:
get:
summary: Get current user info
responses:
'200':
description: Current user
content:
application/json:
schema:
type: object
properties:
username:
type: string
role:
type: string
/api/health:
get:
summary: Server health check
security: []
responses:
'200':
description: Healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [ok, degraded]
db:
type: string
uptime:
type: integer
version:
type: string
sseClients:
type: integer
memory:
type: object
properties:
rss:
type: integer
heapUsed:
type: integer
heapTotal:
type: integer
'503':
description: Database unreachable
/api/data:
get:
summary: Fetch all application data
responses:
'200':
description: Full data snapshot
post:
summary: Full-state save (backup/restore only)
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
'200':
description: Data saved
/api/events:
get:
summary: Server-Sent Events stream for real-time sync
responses:
'200':
description: SSE stream (text/event-stream)
/api/mushroom-strains:
get:
summary: List all Pilzsorten (mushroom strains)
responses:
'200':
description: Array of strains
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/MushroomStrain'
post:
summary: Create a new Pilzsorte
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, kuerzel]
properties:
name:
type: string
description: Must be unique
kuerzel:
type: string
description: Short code, must be unique
description:
type: string
responses:
'200':
description: Strain created
content:
application/json:
schema:
type: object
properties:
id:
type: integer
'400':
description: Validation error (missing fields or unique violation)
/api/mushroom-strains/{id}:
patch:
summary: Update a Pilzsorte
description: >-
Changes to `name` or `kuerzel` cascade into all batches and cultures
that reference this strain (their `species`/`strain` columns are
updated in a single transaction).
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
kuerzel:
type: string
description:
type: string
responses:
'200':
description: Strain updated
'400':
description: Unique violation
delete:
summary: Delete a Pilzsorte (admin only)
description: >-
Fails with 400 when the strain is still referenced by any batch or
culture. Delete or reassign dependent rows first.
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Strain deleted
'400':
description: Strain still in use
'403':
description: Admin role required
/api/staemme:
get:
summary: List all Stämme (strains / genetic lines) of every Pilzsorte
description: >-
The vocabulary for the strain field on batches and cultures. A batch or
culture stores the chosen strain by NAME in `strainText`, not by id, so
rows from before this list existed keep their free text. Also part of
`/api/data` as `staemme`.
responses:
'200':
description: Array of strains
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Stamm'
post:
summary: Add a Stamm to a Pilzsorte
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [strainId, name]
properties:
strainId:
type: integer
description: The Pilzsorte (mushroom_strains.id) the strain belongs to
name:
type: string
maxLength: 60
description: >-
Unique per Pilzsorte, case-insensitive. Surrounding and
repeated whitespace is removed.
notes:
type: string
maxLength: 500
responses:
'200':
description: Strain created
content:
application/json:
schema:
type: object
properties:
id:
type: integer
'400':
description: >-
Validation error — missing fields, unknown Pilzsorte, or
"Stamm: already exists"
/api/staemme/{id}:
patch:
summary: Rename a Stamm or change its notes
description: >-
A new `name` is carried into the `strainText` of every batch and
culture of the same Pilzsorte that carried the old name
(case-insensitive), in one transaction.
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
maxLength: 60
notes:
type: string
maxLength: 500
responses:
'200':
description: Strain updated
'400':
description: 'Empty name, unknown id, or "Stamm: already exists"'
delete:
summary: Remove a Stamm from the list (admin only)
description: >-
Only removes it from the selection. Batches and cultures that carry the
name keep it.
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Strain removed
'403':
description: Admin role required
/api/batches:
post:
summary: Create a new batch
description: >-
Either `strainId` or `species` must be supplied. When `strainId` is set,
`species` and `strain` are auto-filled from the referenced Pilzsorte
and any values passed in the request body are overwritten.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [batchId, qty, days, created, due]
properties:
batchId:
type: string
strainId:
type: integer
description: Pilzsorte id — auto-fills species/strain from mushroom_strains
species:
type: string
description: Required when strainId is omitted
strain:
type: string
description: Kuerzel (free-text fallback when strainId is omitted)
qty:
type: integer
days:
type: integer
bags:
type: array
items:
type: string
created:
type: string
format: date-time
due:
type: string
format: date-time
responses:
'200':
description: Batch created
/api/batches/{batchId}:
patch:
summary: Update batch fields
description: >-
Passing `strainId` switches the batch to a different Pilzsorte and
cascades `species`/`strain` from the mushroom_strains row.
parameters:
- name: batchId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
notes:
type: string
strainId:
type: integer
species:
type: string
strain:
type: string
days:
type: integer
due:
type: string
responses:
'200':
description: Batch updated
delete:
summary: Delete a batch (cascades bags)
parameters:
- name: batchId
in: path
required: true
schema:
type: string
responses:
'200':
description: Batch deleted
/api/batches/{batchId}/bags:
patch:
summary: Add bags to an existing batch
parameters:
- name: batchId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
add:
type: array
items:
type: string
newQty:
type: integer
bagKg:
type: number
description: >-
Weight of each added bag in kg. Omit to reuse the batch's own
weight. Grain spawn is weighed and one batch may hold more
than one size, so the added bags need not match the rest.
minimum: 0
exclusiveMinimum: true
maximum: 1000
responses:
'200':
description: Bags added
/api/scan-log:
post:
summary: Append scan log entries
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
entries:
type: array
items:
type: object
properties:
time:
type: string
action:
type: string
enum: [ADD, MOVE, REMOVE, HARVEST]
batch:
type: string
bag:
type: string
from:
type: string
to:
type: string
responses:
'200':
description: Entries added
content:
application/json:
schema:
type: object
properties:
ids:
type: array
items:
type: integer
delete:
summary: Clear all scan log entries
responses:
'200':
description: Scan log cleared
/api/scan-log/{id}:
delete:
summary: Delete a single scan log entry
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Entry deleted
/api/harvests:
post:
summary: Log a harvest
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [time, grams]
properties:
time:
type: string
format: date-time
batch:
type: string
bag:
type: string
species:
type: string
grams:
type: number
flush:
type: integer
responses:
'200':
description: Harvest logged
/api/block-book:
get:
summary: Blocks marked good or commented, each with its lineage
description: >-
Lineage (substrate mix with target and measured moisture, grain spawn
with its hydration) is read live while the batch exists, otherwise the
snapshot taken when the block was first marked is returned (`live: false`).
responses:
'200':
description: '{ entries: [...] }'
/api/block-notes/good:
post:
summary: Mark a block good, or clear the mark
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [bag]
properties:
bag:
type: string
on:
type: boolean
default: true
responses:
'200':
description: Saved
'404':
description: Unknown bag
/api/block-notes/comments:
post:
summary: Add a comment to a block
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [bag, text]
properties:
bag:
type: string
text:
type: string
maxLength: 2000
flush:
type: integer
description: Set when the comment was written while harvesting
responses:
'200':
description: '{ ok, id }'
'404':
description: Unknown bag
/api/block-notes/comments/{id}:
delete:
summary: Delete a comment (its author or an admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Deleted
'403':
description: Not the author and not an admin
/api/cultures:
post:
summary: Create cultures
description: >-
Pass `strainId` on each culture to auto-fill `species`/`strain` from
the referenced Pilzsorte. Free-text `species`/`strain` are only used
when `strainId` is omitted.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
cultures:
type: array
items:
type: object
required: [id, type]
properties:
id:
type: string
type:
type: string
enum: [MC, PD, LC, G2G]
strainId:
type: integer
description: Pilzsorte id (auto-fills species/strain)
species:
type: string
strain:
type: string
parentId:
type: string
nullable: true
source:
type: string
nullable: true
status:
type: string
enum: [active, stored, used, contam]
notes:
type: string
created:
type: string
format: date-time
responses:
'200':
description: Cultures created
/api/cultures/{id}:
patch:
summary: Update a culture
description: >-
Passing `strainId` switches the culture to a different Pilzsorte and
cascades `species`/`strain` from the mushroom_strains row.
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [active, stored, used, contam]
notes:
type: string
strainId:
type: integer
species:
type: string
strain:
type: string
source:
type: string
nullable: true
responses:
'200':
description: Culture updated
/api/tasks:
post:
summary: Create a task
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [text, created]
properties:
text:
type: string
priority:
type: string
enum: [low, med, high]
created:
type: string
format: date-time
assignee:
type: string
dueDate:
type: string
format: date
responses:
'200':
description: Task created
/api/tasks/{id}:
patch:
summary: Update a task
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
type: object
properties:
done:
type: boolean
text:
type: string
priority:
type: string
assignee:
type: string
dueDate:
type: string
responses:
'200':
description: Task updated
delete:
summary: Delete a task
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Task deleted
/api/team:
post:
summary: Add a team member
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, added]
properties:
name:
type: string
role:
type: string
added:
type: string
format: date-time
responses:
'200':
description: Member added
/api/team/{id}:
delete:
summary: Remove a team member
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Member removed
/api/inventory/delta:
post:
summary: Adjust inventory stock by delta
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [mat, deltaKg]
properties:
mat:
type: string
enum: [hardwood, wheatbran, gypsum, grain]
deltaKg:
type: number
type:
type: string
ref:
type: string
responses:
'200':
description: Stock updated
/api/inventory/set:
post:
summary: Set absolute inventory stock level
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [mat, value]
properties:
mat:
type: string
enum: [hardwood, wheatbran, gypsum, grain]
value:
type: number
responses:
'200':
description: Stock set
/api/inventory/config:
post:
summary: Update inventory thresholds and avg composition
requestBody:
content:
application/json:
schema:
type: object
properties:
thresholds:
type: object
avgComposition:
type: object
responses:
'200':
description: Config updated
/api/calendar-events:
post:
summary: Create a calendar event
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id, title, startDate]
properties:
id:
type: string
title:
type: string
startDate:
type: string
format: date
endDate:
type: string
format: date
allDay:
type: boolean
category:
type: string
assignees:
type: array
items:
type: integer
responses:
'200':
description: Event created
/api/calendar-events/{id}:
patch:
summary: Update a calendar event
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Event updated
delete:
summary: Delete a calendar event
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Event deleted
/api/backup/download:
post:
summary: Download encrypted database backup
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [password]
properties:
password:
type: string
minLength: 8
responses:
'200':
description: Encrypted backup file (AES-256-GCM)
content:
application/octet-stream:
schema:
type: string
format: binary
/api/backup/restore:
post:
summary: Restore database from encrypted backup
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'200':
description: Restore successful
'401':
description: Wrong password
/api/usb-backup:
get:
summary: USB stick backup status (admin)
description: >
Registered sticks with their last result (with `skipped`,
`unreadable` and `missing` when photos on the PC could not be read),
sticks plugged in now (`sameDisk` marks a stick folder on the app's
own disk, which is never backed up to), the run in progress, and
`reminder` — one of running, state, setup, first, error, gaps,
remove, due or null — which the start screen shows. Beside it,
`keyConfirmed` (false shows a line asking to put the recovery key
away), `firstRunPending` (a connected stick waits for "Back up now"),
`stateError` (data/usb-backup.json cannot be read) and `enabled`
(false with USB_BACKUP=off). Changes are announced as the SSE event
`usb-backup`.
responses:
'200':
description: Status
'403':
description: Not an admin
/api/usb-backup/drives:
get:
summary: Drives a stick could be set up on (admin)
responses:
'200':
description: Drives with free space, whether they are the app's own disk, and any stick on them
/api/usb-backup/setup:
post:
summary: Set up the stick in a drive and back up onto it (admin)
description: >
The first stick creates the recovery key; later sticks use the same
key. The key is returned with every setup until it is confirmed as
put away (POST /api/usb-backup/key/confirm), so a first setup that
failed half-way cannot lose it (Cache-Control no-store).
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [root]
properties:
root:
type: string
description: A drive from /api/usb-backup/drives
label:
type: string
maxLength: 40
responses:
'200':
description: >
Stick set up and a backup started. recoveryKey is set until the key
is confirmed; newKey says whether this setup created it.
'400':
description: Not one of the drives (code NOT_A_DRIVE)
'409':
description: >
Refused, with a code (SAME_DISK, NEEDS_KEY, WRONG_KEY, BUSY, FULL,
READ_ONLY, STATE when this PC's record cannot be read or saved …)
/api/usb-backup/connect:
post:
summary: Take back a stick from an earlier installation with its recovery key (admin)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [root, recoveryKey]
properties:
root:
type: string
recoveryKey:
type: string
responses:
'200':
description: >
Connected. Nothing is written to the stick until someone starts the
first backup with POST /api/usb-backup/run (restore first on a new
PC); after that it is backed up every day by itself.
'409':
description: Refused (HAS_KEY, WRONG_KEY, STATE …)
/api/usb-backup/run:
post:
summary: Back up onto a plugged-in stick now (admin)
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: The stick to back up onto; without it, the first registered stick plugged in
force:
type: boolean
description: >
true only for "Back up anyway" after a SHRUNK stop: the run
goes ahead although it finds far less than last time
responses:
'200':
description: The run that started
'409':
description: >
No registered stick plugged in (NOT_PRESENT), only a stick folder
on the app's own disk (SAME_DISK), or a run in progress (BUSY)
/api/usb-backup/key:
post:
summary: Show the recovery key (admin, Cache-Control no-store)
responses:
'200':
description: The key
/api/usb-backup/key/confirm:
post:
summary: Confirm the recovery key is put away (admin)
description: >
Stops the key being returned with every setup and the start-screen
line asking for it. Saved, or not made.
responses:
'200':
description: Confirmed
'409':
description: No recovery key yet, or the confirmation could not be saved (code STATE)
/api/usb-backup/sticks/{id}:
delete:
summary: Forget a lost or retired stick (admin); nothing on the stick is touched
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^[0-9a-f]{16}$'
responses:
'200':
description: Forgotten
'404':
description: Unknown stick
'409':
description: Not forgotten, because this PC's record could not be saved (code STATE)
/api/users:
get:
summary: List all users
responses:
'200':
description: User list (without password hashes)
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username:
type: string
password:
type: string
minLength: 8
role:
type: string
enum: [admin, user]
responses:
'200':
description: User created
/api/users/{id}:
delete:
summary: Delete a user
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: User deleted
/api/usernames:
get:
summary: Get all usernames (lightweight)
responses:
'200':
description: Username list
/api/print:
post:
summary: Send ZPL to label printer
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [zpl]
properties:
zpl:
type: string
responses:
'200':
description: Print job sent
/api/caldav/config:
post:
summary: Update CalDAV configuration
requestBody:
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
responses:
'200':
description: Config updated