{"openapi":"3.1.0","info":{"title":"Café Fausse API","version":"3.1.0","description":"REST API of the Café Fausse application tier. Generated from the code (backend/app/openapi.py). Errors always use the ErrorEnvelope shape."},"servers":[{"url":"/"}],"paths":{"/api/v1/admin/audit":{"get":{"summary":"The audit trail (manager; v9: developers read it too), newest first, each event with its actor's name and roles at the time: from/to (restaurant-local dates, inclusive), actor (a staff email), event (a name or family prefix such as menu. or admin.), paging — plus the grid: q (event, request id, details), filter[col], sort, page/page_size. Staff display names only; no guest PII","tags":["admin"],"operationId":"admin_list_audit_get","responses":{"200":{"description":"Audit events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAuditOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"description":"Global search: event name, request id or details","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: created_at, event, actor, request_id. Default: -created_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 100)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[created_at]","in":"query","required":false,"description":"Day(s), restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[event]","in":"query","required":false,"description":"An event name or family prefix, e.g. admin.customer_ — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"The acting staff member's email (exact) — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"The acting staff account id — one id","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"To"}},{"name":"actor","in":"query","required":false,"schema":{"anyOf":[{"maxLength":254,"type":"string"},{"type":"null"}],"default":null,"title":"Actor"}},{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"maxLength":64,"type":"string"},{"type":"null"}],"default":null,"title":"Event"}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"maximum":500,"minimum":1,"title":"Limit","type":"integer"}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"maximum":1000000,"minimum":0,"title":"Offset","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/auth/login":{"post":{"summary":"Back-office login with email + password (argon2id) — the user must hold a back-office role (viewer, host, manager); sets THE session cookie (__Host-cf_session, SameSite=Lax), the same one /api/v1/auth/login sets","tags":["admin"],"operationId":"admin_login_post","responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSessionOut"}}}},"401":{"description":"Wrong email/password, no back-office role, disabled or locked (one answer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Turnstile verification failed (when enabled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Turnstile token (or cf-turnstile-response in the body)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLoginIn"}}}}}},"/api/v1/admin/auth/logout":{"post":{"summary":"Sign out (THE session: the site too; needs X-CSRF-Token)","tags":["admin"],"operationId":"admin_logout_post","responses":{"204":{"description":"Signed out (also when there was no session)"},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/auth/oidc":{"post":{"summary":"Use the current OpenID Connect sign-in for the back office: v9 has ONE session, so this only checks that the user holds a back-office role (an allow-listed STAFF_EMAILS address got it at sign-in) and answers the back-office view","tags":["admin"],"operationId":"admin_oidc_exchange_post","responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSessionOut"}}}},"401":{"description":"No sign-in session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"No back-office role, or a bad X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Sign-in is not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"description":"csrf_token from GET /api/v1/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/customers":{"post":{"summary":"Add a guest (name + email; phone, notes, newsletter optional). Staff never trigger the welcome email","tags":["admin"],"operationId":"admin_create_customer_post","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"The customer master: one row per guest with newsletter status, account link, first/last visit, visit count, total covers, no-show and cancellation counts and the next booking. Grid: q, filter[col], sort, page","tags":["admin"],"operationId":"admin_list_customers_get","responses":{"200":{"description":"Customers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomersOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16, FR-17","parameters":[{"name":"q","in":"query","required":false,"description":"Global search: name, email, phone or notes","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: name, email, newsletter_status, has_account, upcoming, visit_count, total_covers, no_show_count, cancel_count, reservation_count, first_visit, last_visit, created_at, updated_at. Default: -last_visit,name","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[name]","in":"query","required":false,"description":"Guest name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[email]","in":"query","required":false,"description":"Email — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[phone_number]","in":"query","required":false,"description":"Phone — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[newsletter_status]","in":"query","required":false,"description":"Newsletter — one value or a comma-separated set of subscribed, unsubscribed, never","schema":{"type":"string"}},{"name":"filter[has_account]","in":"query","required":false,"description":"Has a sign-in account (OpenID Connect) — true or false","schema":{"type":"string"}},{"name":"filter[upcoming]","in":"query","required":false,"description":"Has an upcoming booking (sort: by its seating) — true or false","schema":{"type":"string"}},{"name":"filter[erased]","in":"query","required":false,"description":"Erased for privacy — true or false","schema":{"type":"string"}},{"name":"filter[visit_count]","in":"query","required":false,"description":"Visits (past bookings that were not no-shows) — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[total_covers]","in":"query","required":false,"description":"Guests brought over all visits — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[no_show_count]","in":"query","required":false,"description":"No-shows — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[cancel_count]","in":"query","required":false,"description":"Cancellations — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[first_visit]","in":"query","required":false,"description":"First visit day(s) — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[last_visit]","in":"query","required":false,"description":"Last visit day(s) — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[created_at]","in":"query","required":false,"description":"Customer since — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/customers/{customer_id}":{"delete":{"summary":"Privacy erasure (manager only): name, phone, notes and email removed (email → erased-<id>@erased.invalid), newsletter revoked, queued emails redacted, the guest's sign-in accounts removed; reservations and their aggregates are KEPT. Audited","tags":["admin"],"operationId":"admin_erase_customer_delete","responses":{"200":{"description":"Erased (or already erased)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"customer_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"One guest: the list row, their sign-in accounts, the WHOLE reservation history (with emails sent) and their audit trail (ETag = the customer's version)","tags":["admin"],"operationId":"admin_get_customer_get","responses":{"200":{"description":"Customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerDetailOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"customer_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}}],"security":[{"sessionCookie":[]}]},"patch":{"summary":"Update a guest: only the fields sent change (phone/notes \"\" clears; newsletter_signup false = opt-out)","tags":["admin"],"operationId":"admin_update_customer_patch","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16, FR-17","parameters":[{"name":"customer_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/customers/{record}/history":{"get":{"summary":"A customer's change history (who, when, request, field diff); viewers see contact data masked; a privacy erasure scrubs the personal data from it too","tags":["admin"],"operationId":"admin_customer_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Malformed record id/key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"record","in":"path","required":true,"description":"Customer id","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Global search: actor name or request id","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/history":{"get":{"summary":"Every change to every table (manager only). Grid: q, filter[table|operation|actor_kind|actor_id|actor|request_id|changed_at|record_id], sort, page","tags":["admin"],"operationId":"admin_all_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"q","in":"query","required":false,"description":"Global search: table, actor name, request id, record id or the changed values","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor, table, record_id. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[table]","in":"query","required":false,"description":"Only these tables — one value or a comma-separated set of account, allergen, allergen_translation, customer, email_outbox, menu_category, menu_category_translation, menu_item, menu_item_allergen, menu_item_translation, reservation, role, session, staff_account, staff_session, user, user_identity, user_role, user_session","schema":{"type":"string"}},{"name":"filter[record_id]","in":"query","required":false,"description":"The row's own id — one id","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/me":{"get":{"summary":"The signed-in staff member, role and CSRF token","tags":["admin"],"operationId":"admin_me_get","responses":{"200":{"description":"Session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSessionOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"No back-office role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"sessionCookie":[]}]}},"/api/v1/admin/menu":{"get":{"summary":"The whole menu for editing: every locale, unavailable dishes too","tags":["admin"],"operationId":"admin_get_menu_get","responses":{"200":{"description":"Menu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"sessionCookie":[]}]}},"/api/v1/admin/menu/categories":{"post":{"summary":"Create a menu category (manager)","tags":["admin"],"operationId":"admin_create_category_post","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/menu/categories/{slug}":{"delete":{"summary":"Delete an EMPTY menu category (manager)","tags":["admin"],"operationId":"admin_delete_category_delete","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"patch":{"summary":"Rename / reorder a menu category (manager); names: {locale: name | null}","tags":["admin"],"operationId":"admin_update_category_patch","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/menu/items":{"post":{"summary":"Create a dish (manager): category, price, nutrition, allergens, dietary flags, translations","tags":["admin"],"operationId":"admin_create_item_post","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"The dishes as a grid (English texts): q, filter[col] (category, price, calories, availability, dietary, allergens / free_of), sort, page","tags":["admin"],"operationId":"admin_list_menu_items_get","responses":{"200":{"description":"Menu items","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuItemsOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-5","parameters":[{"name":"q","in":"query","required":false,"description":"Global search: slug, English name or description, category","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: slug, name, category, sort_order, price_cents, calories_kcal, is_available, updated_at. Default: category,sort_order","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 100)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[category]","in":"query","required":false,"description":"Category slug(s) — one value or a comma-separated set (lower-case codes)","schema":{"type":"string"}},{"name":"filter[price_cents]","in":"query","required":false,"description":"Price in cents — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[calories_kcal]","in":"query","required":false,"description":"Calories — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[is_available]","in":"query","required":false,"description":"On tonight's menu — true or false","schema":{"type":"string"}},{"name":"filter[contains_alcohol]","in":"query","required":false,"description":"true or false","schema":{"type":"string"}},{"name":"filter[raw_or_undercooked]","in":"query","required":false,"description":"true or false","schema":{"type":"string"}},{"name":"filter[dietary]","in":"query","required":false,"description":"Carries EVERY tag listed — one value or a comma-separated set of vegetarian, vegan, gluten-free, dairy-free, nut-free, pescatarian","schema":{"type":"string"}},{"name":"filter[allergens]","in":"query","required":false,"description":"Contains ANY of these allergen codes — one value or a comma-separated set (lower-case codes)","schema":{"type":"string"}},{"name":"filter[free_of]","in":"query","required":false,"description":"Contains NONE of these allergen codes — one value or a comma-separated set (lower-case codes)","schema":{"type":"string"}},{"name":"filter[updated_at]","in":"query","required":false,"description":"Last edited day(s) — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/menu/items/{record}/history":{"get":{"summary":"A dish's change history: the item, its translations and its allergen links","tags":["admin"],"operationId":"admin_menu_item_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Malformed record id/key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"record","in":"path","required":true,"description":"Menu item id or slug","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Global search: actor name or request id","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/menu/items/{slug}":{"delete":{"summary":"Delete a dish (manager); to hide it for a night use the availability toggle","tags":["admin"],"operationId":"admin_delete_item_delete","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"patch":{"summary":"Change a dish (manager): only the fields sent change (price, nutrition, allergens, flags, texts…)","tags":["admin"],"operationId":"admin_update_item_patch","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/menu/items/{slug}/availability":{"put":{"summary":"The availability toggle (manager): an unavailable dish leaves the public menu at once","tags":["admin"],"operationId":"admin_set_availability_put","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/menu/items/{slug}/translations/{locale}":{"delete":{"summary":"Remove one language of a dish (manager; English cannot be removed — the menu falls back to it)","tags":["admin"],"operationId":"admin_delete_translation_delete","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"locale","in":"path","required":true,"description":"it | es | fr","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"put":{"summary":"Set one language's name/description/notes of a dish (manager)","tags":["admin"],"operationId":"admin_put_translation_put","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMenuChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such menu entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: slug taken / category still has dishes · version_conflict: If-Match is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"slug","in":"path","required":true,"description":"The entry's slug, e.g. ribeye-steak","schema":{"type":"string"}},{"name":"locale","in":"path","required":true,"description":"en | it | es | fr","schema":{"type":"string"}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the entry's updated_at from GET /admin/menu, quoted (\"<updated_at>\"). Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationPutIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/reports":{"get":{"summary":"Canned report: period=today | week | range (from, to): per day reservations, covers, table utilisation, cancellations, newsletter sign-ups, plus totals over the period. The days are a grid: q (date text), filter[col] ranges, sort, page/page_size","tags":["admin"],"operationId":"admin_get_report_get","responses":{"200":{"description":"Report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"description":"Global search: the ISO date (e.g. -12- = every December day)","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: date, reservations, covers, cancellations, newsletter_signups, busiest_slot_tables. Default: date","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 500)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[reservations]","in":"query","required":false,"description":"Bookings — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[covers]","in":"query","required":false,"description":"Guests — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[cancellations]","in":"query","required":false,"description":"Cancellations — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[newsletter_signups]","in":"query","required":false,"description":"Newsletter sign-ups — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[busiest_slot_tables]","in":"query","required":false,"description":"Tables in the busiest seating — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"period","in":"query","required":false,"schema":{"default":"today","enum":["today","week","range"],"title":"Period","type":"string"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"To"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/reports/export":{"get":{"summary":"The same report as CSV (one row per day + a total row; the grid's filters and sort apply, paging does not)","tags":["admin"],"operationId":"admin_export_report_get","responses":{"200":{"description":"text/csv"},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"description":"Global search: the ISO date (e.g. -12- = every December day)","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: date, reservations, covers, cancellations, newsletter_signups, busiest_slot_tables. Default: date","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 500)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[reservations]","in":"query","required":false,"description":"Bookings — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[covers]","in":"query","required":false,"description":"Guests — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[cancellations]","in":"query","required":false,"description":"Cancellations — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[newsletter_signups]","in":"query","required":false,"description":"Newsletter sign-ups — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[busiest_slot_tables]","in":"query","required":false,"description":"Tables in the busiest seating — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"period","in":"query","required":false,"schema":{"default":"today","enum":["today","week","range"],"title":"Period","type":"string"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"To"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/reservations":{"post":{"summary":"Book on a guest's behalf (phone booking, walk-in): same slot rules as the public form; table optional (random free table otherwise); notify emails the confirmation","tags":["admin"],"operationId":"admin_create_staff_booking_post","responses":{"200":{"description":"Replayed (Idempotency-Key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationChangeOut"}}}},"201":{"description":"Booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-8, FR-18, NFR-5","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"UUID; a retried submit books once","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffBookingIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"Reservations grid: from/to (dates, inclusive; default today + 6 days) or filter[time_slot] (.. = all time), status (booked|cancelled|all) or filter[status] (booked, seated, completed, no_show, cancelled), q (name, code, email, phone, notes), filter[col], sort, page/page_size (or limit/offset). Emails masked and phones withheld for viewers","tags":["admin"],"operationId":"admin_list_reservations_get","responses":{"200":{"description":"Reservations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationsOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"description":"Global search: guest name, confirmation code, email, phone or notes","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: time_slot, status, guests, table_number, customer_name, email, confirmation_code, source, created_at, updated_at. Default: time_slot,table_number","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 100)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[time_slot]","in":"query","required":false,"description":"Seating day(s), restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[status]","in":"query","required":false,"description":"Display status — one value or a comma-separated set of booked, seated, completed, no_show, cancelled","schema":{"type":"string"}},{"name":"filter[guests]","in":"query","required":false,"description":"Party size — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[table_number]","in":"query","required":false,"description":"Table 1..30 — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[customer_name]","in":"query","required":false,"description":"Guest name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[email]","in":"query","required":false,"description":"Guest email — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[phone_number]","in":"query","required":false,"description":"Guest phone — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[confirmation_code]","in":"query","required":false,"description":"Confirmation code — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[customer_id]","in":"query","required":false,"description":"One guest's reservations — one id","schema":{"type":"string"}},{"name":"filter[has_account]","in":"query","required":false,"description":"The guest has a sign-in account — true or false","schema":{"type":"string"}},{"name":"filter[newsletter_signup]","in":"query","required":false,"description":"The guest is subscribed — true or false","schema":{"type":"string"}},{"name":"filter[source]","in":"query","required":false,"description":"Who booked — one value or a comma-separated set of web, staff","schema":{"type":"string"}},{"name":"filter[created_at]","in":"query","required":false,"description":"Booked on day(s) — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"To"}},{"name":"status","in":"query","required":false,"schema":{"default":"all","enum":["booked","cancelled","all"],"title":"Status","type":"string"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null"}],"default":null,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"maximum":500,"minimum":1,"title":"Limit","type":"integer"}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"maximum":1000000,"minimum":0,"title":"Offset","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/reservations/{reservation_id}":{"delete":{"summary":"HARD delete (manager only; audited with the deleted booking's facts). Prefer cancel","tags":["admin"],"operationId":"admin_delete_reservation_delete","responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"patch":{"summary":"Edit a reservation: time slot, guests, table (reassignment under the same slot lock as bookings), guest name and phone, notes. Only the fields sent change; notify emails the guest when the seating changed","tags":["admin"],"operationId":"admin_edit_reservation_patch","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-8, NFR-5","parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReservationEditIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"One reservation for the edit form (ETag = its version). Emails masked and phones withheld for viewers","tags":["admin"],"operationId":"admin_get_reservation_get","responses":{"200":{"description":"Reservation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/reservations/{reservation_id}/cancel":{"post":{"summary":"Cancel a reservation (preferred over deleting: history and reports keep it). Body optional: notify, locale","tags":["admin"],"operationId":"admin_cancel_reservation_by_staff_post","responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/reservations/{reservation_id}/status":{"put":{"summary":"Set the status: booked | seated | completed | no_show (attendance, from 12 h before the seating) or cancelled (frees the table; notify emails the guest). A cancellation is final","tags":["admin"],"operationId":"admin_set_reservation_status_put","responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReservationChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not allowed: viewer on a manager action, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict (If-Match is stale; error.current = the record as it is now, for merging) · slot_full · duplicate_booking · conflict (table taken, email taken, erased guest, impossible status change)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The record's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"Optimistic concurrency: the record's \"version\" from the list or GET, quoted. Stale → 409 version_conflict; absent or * → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReservationStatusIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/reservations/{record}/history":{"get":{"summary":"A reservation's change history (who, when, request, field diff); viewers see contact data masked","tags":["admin"],"operationId":"admin_reservation_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Malformed record id/key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"record","in":"path","required":true,"description":"Reservation id or confirmation code","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Global search: actor name or request id","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/reservations/export":{"get":{"summary":"The same filters and sort as /admin/reservations as CSV (all pages, up to 10,000 rows); columns= picks the grid's visible columns (allow-listed, in order)","tags":["admin"],"operationId":"admin_export_reservations_get","responses":{"200":{"description":"text/csv"},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":[{"name":"q","in":"query","required":false,"description":"Global search: guest name, confirmation code, email, phone or notes","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: time_slot, status, guests, table_number, customer_name, email, confirmation_code, source, created_at, updated_at. Default: time_slot,table_number","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 100)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[time_slot]","in":"query","required":false,"description":"Seating day(s), restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[status]","in":"query","required":false,"description":"Display status — one value or a comma-separated set of booked, seated, completed, no_show, cancelled","schema":{"type":"string"}},{"name":"filter[guests]","in":"query","required":false,"description":"Party size — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[table_number]","in":"query","required":false,"description":"Table 1..30 — a number or an inclusive range a..b (either end may be open)","schema":{"type":"string"}},{"name":"filter[customer_name]","in":"query","required":false,"description":"Guest name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[email]","in":"query","required":false,"description":"Guest email — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[phone_number]","in":"query","required":false,"description":"Guest phone — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[confirmation_code]","in":"query","required":false,"description":"Confirmation code — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[customer_id]","in":"query","required":false,"description":"One guest's reservations — one id","schema":{"type":"string"}},{"name":"filter[has_account]","in":"query","required":false,"description":"The guest has a sign-in account — true or false","schema":{"type":"string"}},{"name":"filter[newsletter_signup]","in":"query","required":false,"description":"The guest is subscribed — true or false","schema":{"type":"string"}},{"name":"filter[source]","in":"query","required":false,"description":"Who booked — one value or a comma-separated set of web, staff","schema":{"type":"string"}},{"name":"filter[created_at]","in":"query","required":false,"description":"Booked on day(s) — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"columns","in":"query","required":false,"description":"Comma-separated CSV columns, in order. Allowed: confirmation_code, status, time_slot, table_number, guests, customer_name, email, phone_number, newsletter_signup, created_at, cancelled_at, updated_at, source, notes, has_account, customer_id, reservation_id. Default: confirmation_code, status, time_slot, table_number, guests, customer_name, email, phone_number, newsletter_signup, created_at, cancelled_at","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"format":"date","type":"string"},{"type":"null"}],"default":null,"title":"To"}},{"name":"status","in":"query","required":false,"schema":{"default":"all","enum":["booked","cancelled","all"],"title":"Status","type":"string"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null"}],"default":null,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"maximum":500,"minimum":1,"title":"Limit","type":"integer"}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"maximum":1000000,"minimum":0,"title":"Offset","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/roles":{"get":{"summary":"The roles: whether each may still be assigned (the legacy viewer may not) and how many users hold it","tags":["admin"],"operationId":"admin_list_roles_get","responses":{"200":{"description":"Roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRolesOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","security":[{"sessionCookie":[]}]}},"/api/v1/admin/staff/{record}/history":{"get":{"summary":"A staff account's change history (manager only; password hashes are never part of history)","tags":["admin"],"operationId":"admin_staff_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Malformed record id/key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17","parameters":[{"name":"record","in":"path","required":true,"description":"Staff account id","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Global search: actor name or request id","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/users":{"post":{"summary":"Invite a person with roles (customer · host · manager · developer): an email with a link to set their password is queued (status invited); or give a password (status created). 422 fields: email (email_taken), roles (none)","tags":["admin"],"operationId":"admin_create_user_post","responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreateIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]},"get":{"summary":"Every person who can sign in — customers, staff, developers — with their roles (manager). Grid: q, filter[col], sort, page, page_size","tags":["admin"],"operationId":"admin_list_users_get","responses":{"200":{"description":"Users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUsersOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"q","in":"query","required":false,"description":"Global search: name or email","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: display_name, email, status, last_login_at, created_at, user_id. Default: display_name","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[display_name]","in":"query","required":false,"description":"Part of the name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[email]","in":"query","required":false,"description":"Part of the email — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[roles]","in":"query","required":false,"description":"Holds ANY of these roles — one value or a comma-separated set of customer, viewer, host, manager, developer","schema":{"type":"string"}},{"name":"filter[status]","in":"query","required":false,"description":"The status — one value or a comma-separated set of active, invited, disabled, locked","schema":{"type":"string"}},{"name":"filter[last_login_at]","in":"query","required":false,"description":"Day(s) of the last sign-in, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[created_at]","in":"query","required":false,"description":"Day(s) the user was created, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[user_id]","in":"query","required":false,"description":"One user — one id","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/users/{user_id}":{"get":{"summary":"One user: the row, every role grant (who granted it, when), the OpenID Connect sign-ins and live sessions","tags":["admin"],"operationId":"admin_get_user_get","responses":{"200":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetailOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}}],"security":[{"sessionCookie":[]}]},"patch":{"summary":"Rename a user (If-Match)","tags":["admin"],"operationId":"admin_patch_user_patch","responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict: If-Match is stale (error.current = the user as it is now)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"The user's \"version\" from the list or GET, quoted. Stale → 409; absent → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPatchIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/users/{user_id}/audit":{"get":{"summary":"What the user did and what was done to their account (the audit events, newest first, at most 200)","tags":["admin"],"operationId":"admin_user_audit_get","responses":{"200":{"description":"Events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserAuditOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/users/{user_id}/history":{"get":{"summary":"A user's change history (the ADR 602 shape under `changes`): their row (never the password hash or security stamp) and their role grants","tags":["admin"],"operationId":"admin_user_history_get","responses":{"200":{"description":"History, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserHistoryOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17, FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}},{"name":"q","in":"query","required":false,"description":"Global search: actor name or request id","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Comma-separated keys, '-' = descending, at most 4. Allowed: changed_at, operation, actor. Default: -changed_at","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"1-based page number","schema":{"type":"integer","minimum":1}},{"name":"page_size","in":"query","required":false,"description":"Rows per page, 1..500 (default 50)","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"filter[changed_at]","in":"query","required":false,"description":"Day(s) of the change, restaurant time — a day YYYY-MM-DD or an inclusive range of days a..b (either end may be open; restaurant time)","schema":{"type":"string"}},{"name":"filter[operation]","in":"query","required":false,"description":"What happened to the row — one value or a comma-separated set of insert, update, delete","schema":{"type":"string"}},{"name":"filter[actor_kind]","in":"query","required":false,"description":"Who: the kind of actor — one value or a comma-separated set of staff, account, guest, service, system, database","schema":{"type":"string"}},{"name":"filter[actor_id]","in":"query","required":false,"description":"Who: the actor's id (staff or account id) — one id","schema":{"type":"string"}},{"name":"filter[actor]","in":"query","required":false,"description":"Who: part of the actor's display name — substring, case-insensitive","schema":{"type":"string"}},{"name":"filter[request_id]","in":"query","required":false,"description":"The API request that made the change (exact) — substring, case-insensitive","schema":{"type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/admin/users/{user_id}/password-reset":{"post":{"summary":"Email the user a fresh password-reset link (older links die). revoke_password=true also removes the current password and ends every session. The answer never contains the link or a password","tags":["admin"],"operationId":"admin_reset_user_password_post","responses":{"202":{"description":"sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResetOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: the user is disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/users/{user_id}/roles":{"put":{"summary":"Set the user's roles to EXACTLY this set (permissions are their union; viewer only if they still hold it). New grants record the manager. Takes effect on the user's next request. 422 roles: none · self_manager · last_manager","tags":["admin"],"operationId":"admin_set_user_roles_put","responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict: If-Match is stale (error.current = the user as it is now)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"The user's \"version\" from the list or GET, quoted. Stale → 409; absent → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRolesIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/admin/users/{user_id}/status":{"put":{"summary":"Enable or disable a user (disabling ends every session and voids outstanding password links; enabling clears a lockout). 422 status: self · last_manager","tags":["admin"],"operationId":"admin_set_user_status_put","responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserChangeOut"}}}},"401":{"description":"No admin session, or it expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Not a manager, or a missing/invalid X-CSRF-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"version_conflict: If-Match is stale (error.current = the user as it is now)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-18, NFR-5","parameters":[{"name":"user_id","in":"path","required":true,"description":"The user's id","schema":{"type":"integer","minimum":1}},{"name":"If-Match","in":"header","required":false,"description":"The user's \"version\" from the list or GET, quoted. Stale → 409; absent → unconditional","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from the login answer or GET /admin/me","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserStatusIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/auth/{provider}/callback":{"get":{"summary":"Provider callback: verify state, exchange the code (PKCE), verify the ID token, open THE session of the user (a verified email joins the user of that email; an allow-listed staff email gets its back-office role)","tags":["identity"],"operationId":"auth_finish_login_get","responses":{"303":{"description":"Redirect to return_to (with ?sign_in=failed on any failure)"}},"description":"Requirements: V3.5, V9-USERS, SRS-2.3","parameters":[{"name":"provider","in":"path","required":true,"description":"google | apple | microsoft | linkedin","schema":{"type":"string"}},{"name":"code","in":"query","required":false,"description":"Authorization code","schema":{"type":"string"}},{"name":"state","in":"query","required":false,"description":"Login state","schema":{"type":"string"}}]},"post":{"summary":"Provider callback: verify state, exchange the code (PKCE), verify the ID token, open THE session of the user (a verified email joins the user of that email; an allow-listed staff email gets its back-office role)","tags":["identity"],"operationId":"auth_finish_login_post","responses":{"303":{"description":"Redirect to return_to (with ?sign_in=failed on any failure)"}},"description":"Requirements: V3.5, V9-USERS, SRS-2.3","parameters":[{"name":"provider","in":"path","required":true,"description":"google | apple | microsoft | linkedin","schema":{"type":"string"}},{"name":"code","in":"query","required":false,"description":"Authorization code","schema":{"type":"string"}},{"name":"state","in":"query","required":false,"description":"Login state","schema":{"type":"string"}}]}},"/api/v1/auth/{provider}/login":{"get":{"summary":"Start an OpenID Connect sign-in (302 to the provider; PKCE + state + nonce in a signed cookie)","tags":["identity"],"operationId":"auth_start_login_get","responses":{"302":{"description":"Redirect to the provider"},"404":{"description":"Provider not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5","parameters":[{"name":"provider","in":"path","required":true,"description":"google | apple | microsoft | linkedin","schema":{"type":"string"}},{"name":"return_to","in":"query","required":false,"description":"Site path to come back to, e.g. /en/reservations","schema":{"type":"string"}}]}},"/api/v1/auth/login":{"post":{"summary":"Sign in with email + password — every role (customer, staff, developer); sets the session cookie (Secure, HttpOnly, SameSite=Lax) and answers GET /me's shape with the CSRF token","tags":["identity"],"operationId":"auth_login_post","responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"Wrong email/password, disabled or locked account (deliberately one answer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Turnstile verification failed (when enabled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","parameters":[{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Turnstile token (or cf-turnstile-response in the body)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginIn"}}}}}},"/api/v1/auth/logout":{"post":{"summary":"Sign out (deletes the server-side session; needs X-CSRF-Token from GET /me)","tags":["identity"],"operationId":"auth_logout_post","responses":{"204":{"description":"Signed out (also when there was no session)"},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, V9-USERS, SRS-2.3","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/auth/password/forgot":{"post":{"summary":"Ask for a password-reset link. ALWAYS 202 with the same body — it never says whether the address has an account; at most one link per account per 15 minutes","tags":["identity"],"operationId":"auth_forgot_password_post","responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusOut"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordIn"}}}}}},"/api/v1/auth/password/reset":{"post":{"summary":"Set a new password with the token of a reset or invitation link (one use; every session of the user ends)","tags":["identity"],"operationId":"auth_reset_password_post","responses":{"200":{"description":"reset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusOut"}}}},"422":{"description":"Invalid/expired/used link or a weak password","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordIn"}}}}}},"/api/v1/auth/providers":{"get":{"summary":"Sign-in providers that are configured (empty and enabled=false when OIDC is off; password sign-in is always on)","tags":["identity"],"operationId":"auth_list_providers_get","responses":{"200":{"description":"Providers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvidersOut"}}}}},"description":"Requirements: V3.5"}},"/api/v1/auth/register":{"post":{"summary":"Register a customer account (email + password, at least 12 characters): signed in at once; the email verification link is queued in the outbox (logged until SMTP is configured) — until it is used, /me says verify_email_required and only the bookings made while signed in are listed","tags":["identity"],"operationId":"auth_register_post","responses":{"201":{"description":"Registered and signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"403":{"description":"Turnstile verification failed (when enabled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"conflict: an account with this email exists (sign in, or reset the password)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","parameters":[{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Turnstile token (or cf-turnstile-response in the body)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterIn"}}}}}},"/api/v1/auth/verify-email":{"post":{"summary":"Confirm an email address with the token of the emailed link (no session needed); links the bookings made under that email to the account","tags":["identity"],"operationId":"auth_verify_email_post","responses":{"200":{"description":"verified | already_verified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusOut"}}}},"422":{"description":"Invalid or expired link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenIn"}}}}}},"/api/v1/auth/verify-email/resend":{"post":{"summary":"Send the email verification link again (signed in; at most one per 15 minutes)","tags":["identity"],"operationId":"auth_resend_verification_post","responses":{"202":{"description":"queued | throttled | already_verified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocaleIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/availability":{"get":{"summary":"Bookable slots of a date with free-table counts","tags":["reservations"],"operationId":"api_availability_get","responses":{"200":{"description":"Slots","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityOut"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Database unavailable (Retry-After: 5)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-7","parameters":[{"name":"date","in":"query","required":true,"schema":{"format":"date","title":"Date","type":"string"}}]}},"/api/v1/config":{"get":{"summary":"Public runtime configuration for the web tier: Turnstile site key, which optional features are on","tags":["content"],"operationId":"api_public_config_get","responses":{"200":{"description":"Config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicConfigOut"}}}}}}},"/api/v1/dev/logs":{"get":{"summary":"Recent log lines from Loki, newest first — ONE LogQL template pinned to the namespace: service, level (this or worse), plain-text q, since/until (default: the last hour; at most 7 days), limit. Never arbitrary LogQL; credential-looking values are scrubbed","tags":["developer"],"operationId":"dev_logs_get","responses":{"200":{"description":"Lines (available=false when Loki is off or unreachable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevLogsOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","parameters":[{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"enum":["web","api","mcp","admin","db","outbox","migrate"],"type":"string"},{"type":"null"}],"default":null,"title":"Service"}},{"name":"level","in":"query","required":false,"schema":{"anyOf":[{"enum":["debug","info","warning","error"],"type":"string"},{"type":"null"}],"default":null,"title":"Level"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null"}],"default":null,"title":"Q"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"default":null,"title":"Since"}},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"default":null,"title":"Until"}},{"name":"limit","in":"query","required":false,"schema":{"default":200,"maximum":1000,"minimum":1,"title":"Limit","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/dev/mcp/call":{"post":{"summary":"The MCP tester: ONE tools/call {tool, arguments} through the MCP server with the API's MCP token → {result, is_error, duration_ms, request_id}. A tool that is not annotated read-only also needs the manager role. {method: tools/list} answers the catalogue in result.tools","tags":["developer"],"operationId":"dev_mcp_call_post","responses":{"200":{"description":"The JSON-RPC result (is_error=true + available=false when MCP is off or unreachable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevMcpOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such MCP tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpCallIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/dev/mcp/tools":{"get":{"summary":"The MCP server's tools (tools/list): name, description, JSON Schema of the arguments, read-only flag","tags":["developer"],"operationId":"dev_mcp_tools_get","responses":{"200":{"description":"Tools (available=false when MCP is off or unreachable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevMcpToolsOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","security":[{"sessionCookie":[]}]}},"/api/v1/dev/metrics":{"get":{"summary":"The FIXED metrics catalogue (request rate, p50/p95 latency and error ratio per route; reservation and newsletter outcomes; p95 procedure time; LCP, INP, CLS) with each entry's current series and value. The browser never sends PromQL","tags":["developer"],"operationId":"dev_metrics_get","responses":{"200":{"description":"Catalogue (available=false when Prometheus is off or unreachable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevCatalogueOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","security":[{"sessionCookie":[]}]}},"/api/v1/dev/metrics/{metric_id}":{"get":{"summary":"One catalogue entry over a range: series of [unix seconds, value] points (value in the entry's unit)","tags":["developer"],"operationId":"dev_metric_get","responses":{"200":{"description":"Series","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevMetricOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not in the catalogue","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","parameters":[{"name":"metric_id","in":"path","required":true,"description":"A catalogue id, e.g. http_latency_p95","schema":{"type":"string"}},{"name":"range","in":"query","required":false,"schema":{"default":"1h","enum":["15m","1h","6h","24h","7d"],"title":"Range","type":"string"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/dev/services":{"get":{"summary":"Health of every tier: web, api (version), mcp, admin (their health endpoints), db (schema version, every procedure present), outbox (lag, pending, 24 h outcomes), loki and prometheus (ready)","tags":["developer"],"operationId":"dev_services_get","responses":{"200":{"description":"Services","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevServicesOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","security":[{"sessionCookie":[]}]}},"/api/v1/dev/token-help":{"get":{"summary":"How to obtain the MCP bearer token and the API service token: URLs, the commands to run, where they live — never a token","tags":["developer"],"operationId":"dev_token_help_get","responses":{"200":{"description":"Instructions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevTokenHelpOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in without the developer or manager role (or a bad X-CSRF-Token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-DEV, SRS-2.3, NFR-9","security":[{"sessionCookie":[]}]}},"/api/v1/health":{"get":{"summary":"Liveness (alias of /healthz; no database)","tags":["operations"],"operationId":"api_health_check_get","responses":{"200":{"description":"Alive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthOut"}}}}}}},"/api/v1/me":{"get":{"summary":"The signed-in user: roles (their union is what they may do), verification state, profile and the CSRF token for cookie-authenticated writes","tags":["identity"],"operationId":"auth_me_get","responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, V9-USERS, SRS-2.3","security":[{"sessionCookie":[]}]}},"/api/v1/me/history":{"get":{"summary":"The changes to MY data, newest first: my account, my customer record and my reservations (who: You, Café Fausse staff, the guest form or a service; field before → after)","tags":["me"],"operationId":"auth_my_history_get","responses":{"200":{"description":"History","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyHistoryOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-17, V9-USERS, SRS-2.3","parameters":[{"name":"page","in":"query","required":false,"schema":{"default":1,"maximum":10000,"minimum":1,"title":"Page","type":"integer"}},{"name":"page_size","in":"query","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Page Size","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/me/profile":{"get":{"summary":"My profile: name, phone, newsletter status and the linked customer record","tags":["me"],"operationId":"auth_get_profile_get","responses":{"200":{"description":"Profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V9-USERS, SRS-2.3","security":[{"sessionCookie":[]}]},"patch":{"summary":"Change my name / phone / newsletter (absent = keep, \"\" clears). Name and phone are mine at once and reach the customer record staff see once the email is verified; unsubscribing needs a verified email (403)","tags":["me"],"operationId":"auth_patch_profile_patch","responses":{"200":{"description":"The profile after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16, V9-USERS, SRS-2.3","parameters":[{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePatchIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/me/reservations":{"get":{"summary":"My reservations, newest seating first, with status (booked · seated · completed · no_show · cancelled): all of them, and split into upcoming and past","tags":["me"],"operationId":"auth_my_reservations_get","responses":{"200":{"description":"Reservations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyReservationsOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, V9-USERS, SRS-2.3","security":[{"sessionCookie":[]}]}},"/api/v1/me/reservations/{reservation_id}":{"delete":{"summary":"Cancel one of MY reservations (idempotent; the table is free again at once) — the manage-by-code rules","tags":["me"],"operationId":"auth_cancel_my_reservation_delete","responses":{"200":{"description":"The cancelled booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedReservationOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such reservation of yours (someone else's is the same answer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"reservation_past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: NFR-5, V9-USERS, SRS-2.3","parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The reservation's id (reservation_id in GET /me/reservations)","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"security":[{"sessionCookie":[],"csrfHeader":[]}]},"patch":{"summary":"Move and/or resize one of MY upcoming reservations — the manage-by-code rules (grid, hours, horizon, past, cancelled), the same slot lock and random free table, the same confirmation email","tags":["me"],"operationId":"auth_patch_my_reservation_patch","responses":{"200":{"description":"The booking after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedReservationOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such reservation of yours (someone else's is the same answer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"slot_full · duplicate_booking · reservation_cancelled · reservation_past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-7, NFR-5, V9-USERS, SRS-2.3","parameters":[{"name":"reservation_id","in":"path","required":true,"description":"The reservation's id (reservation_id in GET /me/reservations)","schema":{"type":"string"}},{"name":"X-CSRF-Token","in":"header","required":true,"description":"csrf_token from GET /api/v1/me or the login answer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyReservationPatchIn"}}}},"security":[{"sessionCookie":[],"csrfHeader":[]}]}},"/api/v1/menu":{"get":{"summary":"The menu in one locale: prices, nutrition, allergens, dietary and regulatory flags (ETag-cacheable)","tags":["content"],"operationId":"api_get_menu_get","responses":{"200":{"description":"Menu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuOut"}}}},"304":{"description":"Not modified"},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Database unavailable (Retry-After: 5)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-5, V3.3, V3.4","parameters":[{"name":"If-None-Match","in":"header","required":false,"description":"ETag of a cached copy","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"schema":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}}]}},"/api/v1/newsletter":{"post":{"summary":"Subscribe an email to the newsletter","tags":["newsletter"],"operationId":"api_subscribe_post","responses":{"200":{"description":"Already subscribed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterOut"}}}},"201":{"description":"Subscribed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterOut"}}}},"403":{"description":"verification_failed: Turnstile is enabled and the token is missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-15, FR-16","parameters":[{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Cloudflare Turnstile token when enabled (or cf-turnstile-response in the body)","schema":{"type":"string"}},{"name":"Accept-Language","in":"header","required":false,"description":"The email's language when the body has no locale (en|it|es|fr)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterIn"}}}}}},"/api/v1/newsletter/unsubscribe":{"delete":{"summary":"Unsubscribe from the newsletter with the signed token of an email link (RFC 8058 one-click: POST with the token in the query string and the body List-Unsubscribe=One-Click; or JSON {token}). Idempotent","tags":["newsletter"],"operationId":"api_unsubscribe_delete","responses":{"200":{"description":"Unsubscribed (also when already unsubscribed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeOut"}}}},"422":{"description":"The token is missing, malformed or forged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16","parameters":[{"name":"token","in":"query","required":false,"description":"The signed token from the email's unsubscribe link","schema":{"type":"string"}}]},"post":{"summary":"Unsubscribe from the newsletter with the signed token of an email link (RFC 8058 one-click: POST with the token in the query string and the body List-Unsubscribe=One-Click; or JSON {token}). Idempotent","tags":["newsletter"],"operationId":"api_unsubscribe_post","responses":{"200":{"description":"Unsubscribed (also when already unsubscribed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeOut"}}}},"422":{"description":"The token is missing, malformed or forged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16","parameters":[{"name":"token","in":"query","required":false,"description":"The signed token from the email's unsubscribe link","schema":{"type":"string"}}]}},"/api/v1/newsletter/unsubscribe-request":{"post":{"summary":"Ask for an unsubscribe link without one at hand (CAN-SPAM). Always 202 with the same body: when the address is subscribed, an email with the signed one-click unsubscribe link goes to THAT address (at most one per 15 minutes), so nobody can opt someone else out or learn who is subscribed","tags":["newsletter"],"operationId":"api_unsubscribe_request_post","responses":{"202":{"description":"Accepted (the same answer whether or not the address is subscribed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeRequestOut"}}}},"403":{"description":"verification_failed: Turnstile is enabled and the token is missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-16","parameters":[{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Cloudflare Turnstile token when enabled (or cf-turnstile-response in the body)","schema":{"type":"string"}},{"name":"Accept-Language","in":"header","required":false,"description":"The email's language when the body has no locale (en|it|es|fr)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestEmailIn"}}}}}},"/api/v1/ready":{"get":{"summary":"Readiness (alias of /readyz): sp_health, procedures present, schema at head","tags":["operations"],"operationId":"api_ready_get","responses":{"200":{"description":"Ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyOut"}}}},"503":{"description":"Not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyOut"}}}}}}},"/api/v1/reservations":{"post":{"summary":"Book a table (random free table; idempotent per Idempotency-Key); returns a confirmation code","tags":["reservations"],"operationId":"api_create_reservation_post","responses":{"200":{"description":"Idempotent replay of the same booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingOut"}}}},"201":{"description":"Booked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingOut"}}}},"403":{"description":"verification_failed: Turnstile is enabled and the token is missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"slot_full or duplicate_booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Database unavailable (Retry-After: 5)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: FR-6, FR-7, FR-8, FR-9, FR-18, NFR-5, V3.5","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"A UUID per form submission","schema":{"type":"string"}},{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"Cloudflare Turnstile token when enabled (or cf-turnstile-response in the body)","schema":{"type":"string"}},{"name":"Accept-Language","in":"header","required":false,"description":"The email's language when the body has no locale (en|it|es|fr)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReservationIn"}}}}}},"/api/v1/reservations/{code}":{"delete":{"summary":"Cancel a booking (idempotent); the table is free again at once","tags":["reservations"],"operationId":"api_cancel_reservation_delete","responses":{"200":{"description":"The cancelled booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedReservationOut"}}}},"404":{"description":"Unknown code, or the email does not match (deliberately indistinguishable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"reservation_past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, NFR-5","parameters":[{"name":"code","in":"path","required":true,"description":"Confirmation code, e.g. CF-7K2Q9M (case-insensitive)","schema":{"type":"string"}},{"name":"X-Reservation-Email","in":"header","required":false,"description":"The booking's email (alternative to ?email=/body)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestEmailIn"}}}}},"get":{"summary":"View a booking by confirmation code + email (generic 404 on any mismatch; rate-limited)","tags":["reservations"],"operationId":"api_lookup_reservation_get","responses":{"200":{"description":"The booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedReservationOut"}}}},"404":{"description":"Unknown code, or the email does not match (deliberately indistinguishable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5","parameters":[{"name":"code","in":"path","required":true,"description":"Confirmation code, e.g. CF-7K2Q9M (case-insensitive)","schema":{"type":"string"}},{"name":"X-Reservation-Email","in":"header","required":false,"description":"The booking's email (alternative to ?email=/body)","schema":{"type":"string"}},{"name":"email","in":"query","required":true,"schema":{"format":"email","maxLength":254,"title":"Email","type":"string"}}]},"patch":{"summary":"Move a booking to another slot and/or change the party size (same rules as booking)","tags":["reservations"],"operationId":"api_update_reservation_patch","responses":{"200":{"description":"The updated booking (unchanged when nothing differs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedReservationOut"}}}},"404":{"description":"Unknown code, or the email does not match (deliberately indistinguishable)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"slot_full, duplicate_booking, reservation_cancelled or reservation_past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, NFR-5, FR-8","parameters":[{"name":"code","in":"path","required":true,"description":"Confirmation code, e.g. CF-7K2Q9M (case-insensitive)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReservationUpdateIn"}}}}}},"/api/v1/restaurant":{"get":{"summary":"Public restaurant facts (shared/restaurant.json), ETag-cacheable","tags":["content"],"operationId":"api_restaurant_get","responses":{"200":{"description":"Facts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantOut"}}}},"304":{"description":"Not modified"}},"description":"Requirements: FR-1, FR-2, FR-5, FR-10, FR-14","parameters":[{"name":"If-None-Match","in":"header","required":false,"description":"ETag of a cached copy","schema":{"type":"string"}}]}},"/api/v1/staff/audit":{"get":{"summary":"Staff oversight (read-only): the accounting trail, newest first (no PII in details)","tags":["staff"],"operationId":"auth_staff_audit_get","responses":{"200":{"description":"Audit events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffAuditOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5","parameters":[{"name":"limit","in":"query","required":false,"schema":{"default":100,"maximum":500,"minimum":1,"title":"Limit","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/staff/reservations":{"get":{"summary":"Staff oversight (read-only): reservations from a date for 1..31 days, emails masked","tags":["staff"],"operationId":"auth_staff_reservations_get","responses":{"200":{"description":"Reservations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffReservationsOut"}}}},"401":{"description":"Not signed in, or the session expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Signed in, but not allowed (role / unverified email / bad CSRF token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: V3.5, FR-17","parameters":[{"name":"from","in":"query","required":true,"schema":{"format":"date","title":"From","type":"string"}},{"name":"days","in":"query","required":false,"schema":{"default":1,"maximum":31,"minimum":1,"title":"Days","type":"integer"}}],"security":[{"sessionCookie":[]}]}},"/api/v1/telemetry":{"post":{"summary":"Real-user measurements batch (sendBeacon; text/plain or application/json; ≤ 8 KiB)","tags":["telemetry"],"operationId":"api_ingest_telemetry_post","responses":{"204":{"description":"Accepted"},"413":{"description":"payload_too_large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Validation failed (one message per field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"description":"Requirements: NFR-1","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelemetryBatch"}}}}}},"/api/v1/version":{"get":{"summary":"Build and schema versions (alias of /version, for the web tier's footer)","tags":["operations"],"operationId":"ops_api_version_get","responses":{"200":{"description":"Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionOut"}}}}},"description":"Requirements: NFR-9"}},"/healthz":{"get":{"summary":"Liveness: the process is up (no dependencies)","tags":["operations"],"operationId":"ops_healthz_get","responses":{"200":{"description":"Alive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthOut"}}}}}}},"/readyz":{"get":{"summary":"Readiness: database reachable, every procedure present, schema at the code's Alembic head","tags":["operations"],"operationId":"ops_readyz_get","responses":{"200":{"description":"Ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyOut"}}}},"503":{"description":"Not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyOut"}}}}}}},"/version":{"get":{"summary":"Build and schema versions: API version, git sha, build time, the database's Alembic revision and its migration name, the code's head and whether they match (null schema fields: database unreachable); v9: components = every tier's version (api, db, mcp, web, admin), unavailable ones with the reason","tags":["operations"],"operationId":"ops_version_get","responses":{"200":{"description":"Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionOut"}}}}},"description":"Requirements: NFR-9"}}},"components":{"schemas":{"AdminAuditEventOut":{"additionalProperties":false,"properties":{"audit_id":{"title":"Audit Id","type":"integer"},"event":{"title":"Event","type":"string"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"actor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Actor Id"},"actor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Actor"},"actor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"v9: the actor in words at the time (customers: Customer)","title":"Actor Name"},"actor_roles":{"description":"v9: the actor's roles at the time","items":{"type":"string"},"title":"Actor Roles","type":"array"},"details":{"additionalProperties":true,"title":"Details","type":"object"},"created_at":{"title":"Created At","type":"string"}},"required":["audit_id","event","request_id","actor_id","actor","details","created_at"],"title":"AdminAuditEventOut","type":"object"},"AdminAuditOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"},"limit":{"title":"Limit","type":"integer"},"offset":{"title":"Offset","type":"integer"},"events":{"items":{"$ref":"#/components/schemas/AdminAuditEventOut"},"title":"Events","type":"array"}},"required":["total","from","to","limit","offset","events"],"title":"AdminAuditOut","type":"object"},"AdminConfigOut":{"additionalProperties":false,"properties":{"local_login":{"title":"Local Login","type":"boolean"},"oidc":{"title":"Oidc","type":"boolean"}},"required":["local_login","oidc"],"title":"AdminConfigOut","type":"object"},"AdminCustomerAccountOut":{"additionalProperties":false,"properties":{"provider":{"title":"Provider","type":"string"},"email_verified":{"title":"Email Verified","type":"boolean"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_login_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Login At"}},"required":["provider","email_verified","created_at","last_login_at"],"title":"AdminCustomerAccountOut","type":"object"},"AdminCustomerChangeOut":{"additionalProperties":false,"properties":{"status":{"enum":["created","updated","unchanged","erased","already_erased"],"title":"Status","type":"string"},"customer":{"anyOf":[{"$ref":"#/components/schemas/AdminCustomerOut"},{"type":"null"}]}},"required":["status","customer"],"title":"AdminCustomerChangeOut","type":"object"},"AdminCustomerDetailOut":{"additionalProperties":false,"properties":{"customer":{"$ref":"#/components/schemas/AdminCustomerOut"},"accounts":{"items":{"$ref":"#/components/schemas/AdminCustomerAccountOut"},"title":"Accounts","type":"array"},"reservations":{"items":{"$ref":"#/components/schemas/AdminCustomerReservationOut"},"title":"Reservations","type":"array"},"audit":{"items":{"$ref":"#/components/schemas/AdminAuditEventOut"},"title":"Audit","type":"array"}},"required":["customer","accounts","reservations","audit"],"title":"AdminCustomerDetailOut","type":"object"},"AdminCustomerOut":{"additionalProperties":false,"properties":{"customer_id":{"title":"Customer Id","type":"integer"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"email":{"title":"Email","type":"string"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"newsletter_status":{"enum":["subscribed","unsubscribed","never"],"title":"Newsletter Status","type":"string"},"newsletter_subscribed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Subscribed At"},"newsletter_unsubscribed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newsletter Unsubscribed At"},"has_account":{"title":"Has Account","type":"boolean"},"reservation_count":{"title":"Reservation Count","type":"integer"},"visit_count":{"title":"Visit Count","type":"integer"},"total_covers":{"title":"Total Covers","type":"integer"},"no_show_count":{"title":"No Show Count","type":"integer"},"cancel_count":{"title":"Cancel Count","type":"integer"},"first_visit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Visit"},"last_visit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Visit"},"upcoming_reservation":{"anyOf":[{"$ref":"#/components/schemas/UpcomingReservationOut"},{"type":"null"}]},"erased":{"title":"Erased","type":"boolean"},"erased_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Erased At"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"}},"required":["customer_id","customer_name","email","phone_number","notes","newsletter_status","newsletter_subscribed_at","newsletter_unsubscribed_at","has_account","reservation_count","visit_count","total_covers","no_show_count","cancel_count","first_visit","last_visit","upcoming_reservation","erased","erased_at","created_at","updated_at","version"],"title":"AdminCustomerOut","type":"object"},"AdminCustomerReservationOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"confirmation_code":{"title":"Confirmation Code","type":"string"},"status":{"enum":["booked","seated","completed","no_show","cancelled"],"title":"Status","type":"string"},"time_slot":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Time Slot"},"table_number":{"title":"Table Number","type":"integer"},"guests":{"title":"Guests","type":"integer"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"source":{"enum":["web","staff"],"title":"Source","type":"string"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"cancelled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cancelled At"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"},"emails":{"items":{"$ref":"#/components/schemas/AdminReservationEmailOut"},"title":"Emails","type":"array"}},"required":["reservation_id","confirmation_code","status","time_slot","table_number","guests","notes","source","created_at","updated_at","cancelled_at","version","emails"],"title":"AdminCustomerReservationOut","type":"object"},"AdminCustomersOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"customers":{"items":{"$ref":"#/components/schemas/AdminCustomerOut"},"title":"Customers","type":"array"}},"required":["total","customers"],"title":"AdminCustomersOut","type":"object"},"AdminHistoryActorOut":{"additionalProperties":false,"properties":{"kind":{"description":"staff (back office), account (signed-in guest), guest (web form), service (an authenticated API client such as the MCP server: name/via = its name), system (a job: via names it), database (a direct write by a database login: via names the role)","enum":["staff","account","guest","service","system","database"],"title":"Kind","type":"string"},"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"The staff or account id (a guest's customer id when known)","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Display name (staff: their name or masked email)","title":"Name"},"via":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Channel or process: web, menu-import, email-sender, cli, login-policy, …","title":"Via"}},"required":["kind","id","name","via"],"title":"AdminHistoryActorOut","type":"object"},"AdminHistoryChangeOut":{"additionalProperties":false,"properties":{"field":{"title":"Field","type":"string"},"before":{"description":"Value before (null for an insert); masked for viewers; \"[erased]\" after a privacy erasure; \"[redacted]\" for secrets","title":"Before"},"after":{"description":"Value after (null for a delete)","title":"After"}},"required":["field","before","after"],"title":"AdminHistoryChangeOut","type":"object"},"AdminHistoryEntryOut":{"additionalProperties":false,"properties":{"id":{"description":"<table>:<history row id>","title":"Id","type":"string"},"table":{"description":"The changed table, without the tb_ prefix: customer, reservation, menu_item, …","title":"Table","type":"string"},"record_id":{"description":"The record the entry belongs to (a menu item's id for its translations)","title":"Record Id","type":"integer"},"row_id":{"description":"The changed row's own primary key","title":"Row Id","type":"integer"},"operation":{"description":"Lower case, like filter[operation]","enum":["insert","update","delete"],"title":"Operation","type":"string"},"changed_at":{"title":"Changed At","type":"string"},"actor":{"$ref":"#/components/schemas/AdminHistoryActorOut"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"txid":{"description":"PostgreSQL transaction id: entries with the same txid were one change","title":"Txid","type":"integer"},"changes":{"items":{"$ref":"#/components/schemas/AdminHistoryChangeOut"},"title":"Changes","type":"array"},"scrubbed":{"description":"Personal data was removed from this entry by a privacy erasure","title":"Scrubbed","type":"boolean"}},"required":["id","table","record_id","row_id","operation","changed_at","actor","request_id","txid","changes","scrubbed"],"title":"AdminHistoryEntryOut","type":"object"},"AdminHistoryOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"entity":{"anyOf":[{"enum":["reservation","customer","menu_item","staff","user"],"type":"string"},{"type":"null"}],"title":"Entity"},"record":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Record"},"entries":{"items":{"$ref":"#/components/schemas/AdminHistoryEntryOut"},"title":"Entries","type":"array"}},"required":["total","entity","record","entries"],"title":"AdminHistoryOut","type":"object"},"AdminLoginIn":{"description":"POST /admin/auth/login. ``cf-turnstile-response`` is accepted (and read separately) when Turnstile is on.","properties":{"email":{"maxLength":254,"title":"Email","type":"string"},"password":{"maxLength":1024,"minLength":1,"title":"Password","type":"string"},"cf-turnstile-response":{"anyOf":[{"maxLength":2048,"type":"string"},{"type":"null"}],"default":null,"title":"Cf-Turnstile-Response"}},"required":["email","password"],"title":"AdminLoginIn","type":"object"},"AdminMenuChangeOut":{"additionalProperties":false,"properties":{"status":{"title":"Status","type":"string"},"category":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"title":"Category"},"item":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"title":"Item"}},"required":["status"],"title":"AdminMenuChangeOut","type":"object"},"AdminMenuItemRowOut":{"additionalProperties":false,"properties":{"slug":{"title":"Slug","type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"category":{"title":"Category","type":"string"},"category_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category Name"},"sort_order":{"title":"Sort Order","type":"integer"},"price_cents":{"title":"Price Cents","type":"integer"},"calories_kcal":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Calories Kcal"},"is_available":{"title":"Is Available","type":"boolean"},"contains_alcohol":{"title":"Contains Alcohol","type":"boolean"},"raw_or_undercooked":{"title":"Raw Or Undercooked","type":"boolean"},"dietary":{"items":{"type":"string"},"title":"Dietary","type":"array"},"allergens":{"items":{"type":"string"},"title":"Allergens","type":"array"},"locales":{"items":{"type":"string"},"title":"Locales","type":"array"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"required":["slug","name","category","category_name","sort_order","price_cents","calories_kcal","is_available","contains_alcohol","raw_or_undercooked","dietary","allergens","locales","updated_at"],"title":"AdminMenuItemRowOut","type":"object"},"AdminMenuItemsOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"items":{"items":{"$ref":"#/components/schemas/AdminMenuItemRowOut"},"title":"Items","type":"array"}},"required":["total","items"],"title":"AdminMenuItemsOut","type":"object"},"AdminMenuOut":{"additionalProperties":true,"description":"The whole menu, every locale, unavailable items included (sp_admin_menu_document; see that procedure).","properties":{"allergens":{"items":{"additionalProperties":true,"type":"object"},"title":"Allergens","type":"array"},"categories":{"items":{"additionalProperties":true,"type":"object"},"title":"Categories","type":"array"}},"required":["allergens","categories"],"title":"AdminMenuOut","type":"object"},"AdminReservationChangeOut":{"additionalProperties":false,"properties":{"status":{"enum":["booked","replayed","updated","unchanged","cancelled"],"title":"Status","type":"string"},"reservation":{"$ref":"#/components/schemas/AdminReservationOut"}},"required":["status","reservation"],"title":"AdminReservationChangeOut","type":"object"},"AdminReservationEmailOut":{"additionalProperties":false,"properties":{"template":{"enum":["reservation_confirmed","reservation_modified","reservation_cancelled","newsletter_welcome"],"title":"Template","type":"string"},"status":{"enum":["pending","sending","sent","logged","failed"],"title":"Status","type":"string"}},"required":["template","status"],"title":"AdminReservationEmailOut","type":"object"},"AdminReservationOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"confirmation_code":{"title":"Confirmation Code","type":"string"},"status":{"enum":["booked","seated","completed","no_show","cancelled"],"title":"Status","type":"string"},"time_slot":{"title":"Time Slot","type":"string"},"table_number":{"title":"Table Number","type":"integer"},"guests":{"title":"Guests","type":"integer"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"email":{"title":"Email","type":"string"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"newsletter_signup":{"title":"Newsletter Signup","type":"boolean"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"cancelled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cancelled At"},"emails":{"items":{"$ref":"#/components/schemas/AdminReservationEmailOut"},"title":"Emails","type":"array"},"customer_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Customer Id"},"has_account":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Has Account"},"customer_erased":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Customer Erased"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Notes"},"source":{"anyOf":[{"enum":["web","staff"],"type":"string"},{"type":"null"}],"default":null,"title":"Source"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Version"}},"required":["reservation_id","confirmation_code","status","time_slot","table_number","guests","customer_name","email","phone_number","newsletter_signup","created_at","updated_at","cancelled_at","emails"],"title":"AdminReservationOut","type":"object"},"AdminReservationsOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"},"status":{"enum":["booked","cancelled","all"],"title":"Status","type":"string"},"limit":{"title":"Limit","type":"integer"},"offset":{"title":"Offset","type":"integer"},"reservations":{"items":{"$ref":"#/components/schemas/AdminReservationOut"},"title":"Reservations","type":"array"}},"required":["total","from","to","status","limit","offset","reservations"],"title":"AdminReservationsOut","type":"object"},"AdminRoleOut":{"additionalProperties":false,"properties":{"role":{"title":"Role","type":"string"},"display_name":{"title":"Display Name","type":"string"},"staff_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staff Rank"},"assignable":{"title":"Assignable","type":"boolean"},"users":{"title":"Users","type":"integer"}},"required":["role","display_name","staff_rank","assignable","users"],"title":"AdminRoleOut","type":"object"},"AdminRolesOut":{"additionalProperties":false,"properties":{"roles":{"items":{"$ref":"#/components/schemas/AdminRoleOut"},"title":"Roles","type":"array"}},"required":["roles"],"title":"AdminRolesOut","type":"object"},"AdminSessionOut":{"additionalProperties":false,"properties":{"user_id":{"title":"User Id","type":"integer"},"roles":{"description":"v9: every role of the user (permissions = their union)","items":{"type":"string"},"title":"Roles","type":"array"},"email":{"title":"Email","type":"string"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"role":{"description":"The highest back-office rank","enum":["viewer","host","manager","developer"],"title":"Role","type":"string"},"auth_method":{"enum":["password","oidc","register"],"title":"Auth Method","type":"string"},"csrf_token":{"title":"Csrf Token","type":"string"},"expires_at":{"title":"Expires At","type":"string"}},"required":["user_id","roles","email","display_name","role","auth_method","csrf_token","expires_at"],"title":"AdminSessionOut","type":"object"},"AdminUserAuditOut":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/components/schemas/AdminAuditEventOut"},"title":"Events","type":"array"}},"required":["events"],"title":"AdminUserAuditOut","type":"object"},"AdminUserChangeOut":{"additionalProperties":false,"properties":{"status":{"title":"Status","type":"string"},"user":{"anyOf":[{"$ref":"#/components/schemas/AdminUserOut"},{"type":"null"}],"default":null}},"required":["status"],"title":"AdminUserChangeOut","type":"object"},"AdminUserDetailOut":{"additionalProperties":false,"properties":{"user":{"$ref":"#/components/schemas/AdminUserOut"},"roles":{"items":{"$ref":"#/components/schemas/AdminUserRoleGrantOut"},"title":"Roles","type":"array"},"identities":{"items":{"$ref":"#/components/schemas/AdminUserIdentityOut"},"title":"Identities","type":"array"},"sessions":{"title":"Sessions","type":"integer"}},"required":["user","roles","identities","sessions"],"title":"AdminUserDetailOut","type":"object"},"AdminUserHistoryOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"entity":{"anyOf":[{"enum":["reservation","customer","menu_item","staff","user"],"type":"string"},{"type":"null"}],"title":"Entity"},"record":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Record"},"entries":{"items":{"$ref":"#/components/schemas/AdminHistoryEntryOut"},"title":"Entries","type":"array"},"changes":{"description":"The same entries, under the admin's name for them","items":{"$ref":"#/components/schemas/AdminHistoryEntryOut"},"title":"Changes","type":"array"}},"required":["total","entity","record","entries","changes"],"title":"AdminUserHistoryOut","type":"object"},"AdminUserIdentityOut":{"additionalProperties":false,"properties":{"provider":{"title":"Provider","type":"string"},"email_verified":{"title":"Email Verified","type":"boolean"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_login_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Login At"}},"required":["provider","email_verified","created_at","last_login_at"],"title":"AdminUserIdentityOut","type":"object"},"AdminUserOut":{"additionalProperties":false,"description":"ADR 701 §2's user — what admin/src/api/users.js normalizeUser reads — plus a few facts more.","properties":{"user_id":{"title":"User Id","type":"integer"},"email":{"title":"Email","type":"string"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"roles":{"items":{"type":"string"},"title":"Roles","type":"array"},"status":{"enum":["active","invited","disabled","locked"],"title":"Status","type":"string"},"sso":{"description":"Signs in with an OpenID Connect provider","title":"Sso","type":"boolean"},"last_login_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Login At"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The If-Match of every write to this user","title":"Version"},"staff_role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Staff Role"},"email_verified_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Verified At"},"has_password":{"title":"Has Password","type":"boolean"},"providers":{"items":{"type":"string"},"title":"Providers","type":"array"},"customer_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Customer Id"},"locked":{"title":"Locked","type":"boolean"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"required":["user_id","email","display_name","roles","status","sso","last_login_at","created_at","version","staff_role","email_verified_at","has_password","providers","customer_id","locked","updated_at"],"title":"AdminUserOut","type":"object"},"AdminUserResetOut":{"additionalProperties":false,"properties":{"status":{"const":"sent","title":"Status","type":"string"},"expires_at":{"title":"Expires At","type":"string"}},"required":["status","expires_at"],"title":"AdminUserResetOut","type":"object"},"AdminUserRoleGrantOut":{"additionalProperties":false,"properties":{"role":{"title":"Role","type":"string"},"granted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Granted At"},"granted_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Granted By"}},"required":["role","granted_at","granted_by"],"title":"AdminUserRoleGrantOut","type":"object"},"AdminUsersOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"users":{"items":{"$ref":"#/components/schemas/AdminUserOut"},"title":"Users","type":"array"}},"required":["total","users"],"title":"AdminUsersOut","type":"object"},"AllergenOut":{"additionalProperties":false,"properties":{"code":{"title":"Code","type":"string"},"name":{"title":"Name","type":"string"}},"required":["code","name"],"title":"AllergenOut","type":"object"},"ApiCallEvent":{"additionalProperties":false,"properties":{"type":{"const":"api_call","title":"Type","type":"string"},"endpoint":{"pattern":"^/api/v1/[a-z0-9/_-]{0,40}$","title":"Endpoint","type":"string"},"method":{"enum":["GET","POST"],"title":"Method","type":"string"},"status":{"maximum":599,"minimum":0,"title":"Status","type":"integer"},"duration_ms":{"maximum":600000,"minimum":0,"title":"Duration Ms","type":"number"}},"required":["type","endpoint","method","status","duration_ms"],"title":"ApiCallEvent","type":"object"},"AuditEventOut":{"additionalProperties":false,"properties":{"audit_id":{"title":"Audit Id","type":"integer"},"event":{"title":"Event","type":"string"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"details":{"additionalProperties":true,"title":"Details","type":"object"},"created_at":{"title":"Created At","type":"string"}},"required":["audit_id","event","request_id","details","created_at"],"title":"AuditEventOut","type":"object"},"AvailabilityIn":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"}},"required":["available"],"title":"AvailabilityIn","type":"object"},"AvailabilityOut":{"additionalProperties":false,"properties":{"date":{"title":"Date","type":"string"},"timezone":{"title":"Timezone","type":"string"},"slots":{"items":{"$ref":"#/components/schemas/SlotOut"},"title":"Slots","type":"array"}},"required":["date","timezone","slots"],"title":"AvailabilityOut","type":"object"},"BookingOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"table_number":{"title":"Table Number","type":"integer"},"time_slot":{"title":"Time Slot","type":"string"},"guests":{"title":"Guests","type":"integer"},"customer_name":{"title":"Customer Name","type":"string"},"confirmation_code":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CF-XXXXXX — with the email, lets the guest view/modify/cancel","title":"Confirmation Code"}},"required":["reservation_id","table_number","time_slot","guests","customer_name","confirmation_code"],"title":"BookingOut","type":"object"},"CategoryIn":{"additionalProperties":false,"description":"POST /admin/menu/categories (``slug`` + ``names.en`` required) and PATCH (everything optional).","properties":{"slug":{"anyOf":[{"maxLength":60,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Slug"},"sort_order":{"anyOf":[{"maximum":999,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Sort Order"},"names":{"anyOf":[{"additionalProperties":{"anyOf":[{"maxLength":80,"minLength":1,"type":"string"},{"type":"null"}]},"propertyNames":{"enum":["en","it","es","fr"]},"type":"object"},{"type":"null"}],"default":null,"title":"Names"}},"title":"CategoryIn","type":"object"},"ClickEvent":{"additionalProperties":false,"properties":{"type":{"const":"click","title":"Type","type":"string"},"control":{"pattern":"^[a-z0-9][a-z0-9_.-]{0,39}$","title":"Control","type":"string"},"route":{"pattern":"^/[a-z0-9/_-]{0,60}$","title":"Route","type":"string"}},"required":["type","control","route"],"title":"ClickEvent","type":"object"},"ComponentVersionOut":{"additionalProperties":false,"description":"One tier's version: status ok (+ what it reports) or unavailable + the reason. db adds the schema fields.","properties":{"status":{"enum":["ok","unavailable"],"title":"Status","type":"string"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Reason"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Name"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Version"},"git_sha":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Git Sha"},"build_time":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Build Time"},"schema_version":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Schema Version"},"migration":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Migration"},"schema_head":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Schema Head"},"schema_current":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Schema Current"}},"required":["status"],"title":"ComponentVersionOut","type":"object"},"CustomerIn":{"additionalProperties":false,"description":"POST /admin/customers (``customer_name`` + ``email`` required) and PATCH /admin/customers/{id} (any subset;\n``phone_number``/``notes`` \"\" clears). ``newsletter_signup`` false records an opt-out.","properties":{"customer_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Customer Name"},"email":{"anyOf":[{"format":"email","maxLength":254,"type":"string"},{"type":"null"}],"default":null,"title":"Email"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"notes":{"anyOf":[{"maxLength":1000,"type":"string"},{"type":"null"}],"default":null,"title":"Notes"},"newsletter_signup":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Newsletter Signup"}},"title":"CustomerIn","type":"object"},"DevCatalogueEntryOut":{"additionalProperties":false,"properties":{"id":{"title":"Id","type":"string"},"group":{"title":"Group","type":"string"},"unit":{"title":"Unit","type":"string"},"kind":{"enum":["line","bar"],"title":"Kind","type":"string"},"description":{"title":"Description","type":"string"},"series":{"description":"The series it has now (routes, outcomes, procedures, or p75)","items":{"type":"string"},"title":"Series","type":"array"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Now: the total (rates, counts) or the worst series (latencies, ratios)","title":"Value"}},"required":["id","group","unit","kind","description","series","value"],"title":"DevCatalogueEntryOut","type":"object"},"DevCatalogueOut":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"},"catalogue":{"items":{"$ref":"#/components/schemas/DevCatalogueEntryOut"},"title":"Catalogue","type":"array"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Message"}},"required":["available","catalogue"],"title":"DevCatalogueOut","type":"object"},"DevLogLineOut":{"additionalProperties":false,"properties":{"ts":{"title":"Ts","type":"string"},"service":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service"},"level":{"title":"Level","type":"string"},"message":{"title":"Message","type":"string"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"fields":{"additionalProperties":true,"title":"Fields","type":"object"}},"required":["ts","service","level","message","request_id","fields"],"title":"DevLogLineOut","type":"object"},"DevLogsOut":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"},"query":{"description":"The LogQL the API ran (built from the template, never from the caller)","title":"Query","type":"string"},"lines":{"items":{"$ref":"#/components/schemas/DevLogLineOut"},"title":"Lines","type":"array"},"truncated":{"title":"Truncated","type":"boolean"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Message"}},"required":["available","query","lines","truncated"],"title":"DevLogsOut","type":"object"},"DevMcpOut":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"},"tool":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tool"},"read_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Read Only"},"result":{"title":"Result"},"is_error":{"title":"Is Error","type":"boolean"},"duration_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration Ms"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"}},"required":["available","tool","read_only","result","is_error","duration_ms","request_id"],"title":"DevMcpOut","type":"object"},"DevMcpToolOut":{"additionalProperties":false,"properties":{"name":{"title":"Name","type":"string"},"description":{"title":"Description","type":"string"},"input_schema":{"additionalProperties":true,"title":"Input Schema","type":"object"},"read_only":{"title":"Read Only","type":"boolean"}},"required":["name","description","input_schema","read_only"],"title":"DevMcpToolOut","type":"object"},"DevMcpToolsOut":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"},"tools":{"items":{"$ref":"#/components/schemas/DevMcpToolOut"},"title":"Tools","type":"array"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Message"}},"required":["available","tools"],"title":"DevMcpToolsOut","type":"object"},"DevMetricOut":{"additionalProperties":false,"properties":{"available":{"title":"Available","type":"boolean"},"id":{"title":"Id","type":"string"},"unit":{"title":"Unit","type":"string"},"step":{"title":"Step","type":"integer"},"range":{"title":"Range","type":"string"},"series":{"items":{"$ref":"#/components/schemas/DevSeriesOut"},"title":"Series","type":"array"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Message"}},"required":["available","id","unit","step","range","series"],"title":"DevMetricOut","type":"object"},"DevSeriesOut":{"additionalProperties":false,"properties":{"name":{"title":"Name","type":"string"},"points":{"items":{"items":{"type":"number"},"type":"array"},"title":"Points","type":"array"}},"required":["name","points"],"title":"DevSeriesOut","type":"object"},"DevServiceOut":{"additionalProperties":false,"properties":{"name":{"title":"Name","type":"string"},"health":{"enum":["ok","degraded","down","unknown"],"title":"Health","type":"string"},"ready":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ready"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"},"detail":{"additionalProperties":true,"title":"Detail","type":"object"},"checked_at":{"title":"Checked At","type":"string"}},"required":["name","health","ready","version","detail","checked_at"],"title":"DevServiceOut","type":"object"},"DevServicesOut":{"additionalProperties":false,"properties":{"services":{"items":{"$ref":"#/components/schemas/DevServiceOut"},"title":"Services","type":"array"}},"required":["services"],"title":"DevServicesOut","type":"object"},"DevTokenHelpOut":{"additionalProperties":false,"properties":{"api_base":{"title":"Api Base","type":"string"},"docs_url":{"title":"Docs Url","type":"string"},"openapi_url":{"title":"Openapi Url","type":"string"},"mcp_url":{"title":"Mcp Url","type":"string"},"mcp_token_env":{"title":"Mcp Token Env","type":"string"},"commands":{"additionalProperties":{"type":"string"},"title":"Commands","type":"object"},"service_token":{"additionalProperties":{"type":"string"},"title":"Service Token","type":"object"},"docs":{"title":"Docs","type":"string"},"docs_console":{"title":"Docs Console","type":"string"}},"required":["api_base","docs_url","openapi_url","mcp_url","mcp_token_env","commands","service_token","docs","docs_console"],"title":"DevTokenHelpOut","type":"object"},"ErrorBody":{"additionalProperties":false,"properties":{"code":{"enum":["slot_full","duplicate_booking","validation_error","not_found","rate_limited","payload_too_large","internal_error","reservation_cancelled","reservation_past","unauthorized","forbidden","conflict","verification_failed","version_conflict"],"title":"Code","type":"string"},"message":{"title":"Message","type":"string"},"fields":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Fields"},"current":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"description":"409 version_conflict on a staff record only: the record as the server holds it now (the GET view)","title":"Current"}},"required":["code","message"],"title":"ErrorBody","type":"object"},"ErrorEnvelope":{"additionalProperties":false,"properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}},"required":["error"],"title":"ErrorEnvelope","type":"object"},"FeatureFlagOut":{"additionalProperties":false,"properties":{"enabled":{"title":"Enabled","type":"boolean"}},"required":["enabled"],"title":"FeatureFlagOut","type":"object"},"ForgotPasswordIn":{"description":"POST /auth/password/forgot — always answered 202 (never says whether the address has an account).","properties":{"email":{"maxLength":254,"title":"Email","type":"string"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"required":["email"],"title":"ForgotPasswordIn","type":"object"},"GuestEmailIn":{"description":"The email that proves a guest owns a booking (GET query / DELETE body / X-Reservation-Email header).","properties":{"email":{"format":"email","maxLength":254,"title":"Email","type":"string"}},"required":["email"],"title":"GuestEmailIn","type":"object"},"HealthOut":{"additionalProperties":false,"properties":{"status":{"const":"ok","title":"Status","type":"string"}},"required":["status"],"title":"HealthOut","type":"object"},"ItemIn":{"additionalProperties":false,"description":"POST /admin/menu/items (``slug``, ``category``, ``price_cents``, ``translations.en`` required) and PATCH\n/admin/menu/items/{slug} (any subset: only the keys sent change).","properties":{"slug":{"anyOf":[{"maxLength":60,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Slug"},"category":{"anyOf":[{"maxLength":60,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Category"},"sort_order":{"anyOf":[{"maximum":999,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Sort Order"},"price_cents":{"anyOf":[{"maximum":1000000,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Price Cents"},"calories_kcal":{"anyOf":[{"maximum":10000,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Calories Kcal"},"nutrition":{"anyOf":[{"$ref":"#/components/schemas/NutritionIn"},{"type":"null"}],"default":null},"contains_alcohol":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Contains Alcohol"},"raw_or_undercooked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Raw Or Undercooked"},"dietary":{"anyOf":[{"items":{"enum":["vegetarian","vegan","gluten-free","dairy-free","nut-free","pescatarian"],"type":"string"},"maxItems":6,"type":"array"},{"type":"null"}],"default":null,"title":"Dietary"},"allergens":{"anyOf":[{"items":{"maxLength":40,"pattern":"^[a-z]+(-[a-z]+)*$","type":"string"},"maxItems":9,"type":"array"},{"type":"null"}],"default":null,"title":"Allergens"},"is_available":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Is Available"},"translations":{"anyOf":[{"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/ItemTranslationIn"},{"type":"null"}]},"propertyNames":{"enum":["en","it","es","fr"]},"type":"object"},{"type":"null"}],"default":null,"title":"Translations"}},"title":"ItemIn","type":"object"},"ItemTranslationIn":{"additionalProperties":false,"properties":{"name":{"maxLength":120,"minLength":1,"title":"Name","type":"string"},"description":{"default":"","maxLength":600,"title":"Description","type":"string"},"chef_note":{"anyOf":[{"maxLength":1000,"type":"string"},{"type":"null"}],"default":null,"title":"Chef Note"},"advisory_note":{"anyOf":[{"maxLength":400,"type":"string"},{"type":"null"}],"default":null,"title":"Advisory Note"}},"required":["name"],"title":"ItemTranslationIn","type":"object"},"LocaleIn":{"properties":{"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"title":"LocaleIn","type":"object"},"LoginIn":{"description":"POST /auth/login (every role) and /admin/auth/login (back-office roles only).","properties":{"email":{"maxLength":254,"title":"Email","type":"string"},"password":{"maxLength":1024,"minLength":1,"title":"Password","type":"string"},"cf-turnstile-response":{"anyOf":[{"maxLength":2048,"type":"string"},{"type":"null"}],"default":null,"title":"Cf-Turnstile-Response"}},"required":["email","password"],"title":"LoginIn","type":"object"},"ManagedReservationOut":{"additionalProperties":false,"properties":{"confirmation_code":{"title":"Confirmation Code","type":"string"},"status":{"enum":["booked","cancelled"],"title":"Status","type":"string"},"time_slot":{"title":"Time Slot","type":"string"},"guests":{"title":"Guests","type":"integer"},"table_number":{"title":"Table Number","type":"integer"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"cancelled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cancelled At"}},"required":["confirmation_code","status","time_slot","guests","table_number","customer_name","created_at","updated_at","cancelled_at"],"title":"ManagedReservationOut","type":"object"},"McpCallIn":{"description":"POST /dev/mcp/call — ``{tool, arguments}``; ``method: tools/list`` (or tool ``list_tools``) = the catalogue.","properties":{"method":{"anyOf":[{"enum":["tools/list","tools/call"],"type":"string"},{"type":"null"}],"default":null,"title":"Method"},"tool":{"anyOf":[{"maxLength":64,"type":"string"},{"type":"null"}],"default":null,"title":"Tool"},"arguments":{"additionalProperties":true,"title":"Arguments","type":"object"}},"title":"McpCallIn","type":"object"},"MeOut":{"additionalProperties":false,"description":"v9: THE signed-in user (any role) — the same answer after /auth/login, /auth/register and OIDC.","properties":{"user_id":{"title":"User Id","type":"integer"},"email":{"title":"Email","type":"string"},"email_verified":{"title":"Email Verified","type":"boolean"},"verify_email_required":{"description":"Show the 'verify your email' banner","title":"Verify Email Required","type":"boolean"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The OpenID Connect provider of this session (null = password)","title":"Provider"},"auth_method":{"enum":["password","oidc","register"],"title":"Auth Method","type":"string"},"roles":{"description":"Every role the user holds; permissions are their union","items":{"enum":["customer","viewer","host","manager","developer"],"type":"string"},"title":"Roles","type":"array"},"staff_role":{"anyOf":[{"enum":["viewer","host","manager"],"type":"string"},{"type":"null"}],"description":"Highest back-office role, if any","title":"Staff Role"},"csrf_token":{"title":"Csrf Token","type":"string"},"expires_at":{"title":"Expires At","type":"string"},"profile":{"anyOf":[{"$ref":"#/components/schemas/ProfileOut"},{"type":"null"}],"default":null}},"required":["user_id","email","email_verified","verify_email_required","display_name","provider","auth_method","roles","staff_role","csrf_token","expires_at"],"title":"MeOut","type":"object"},"MenuCategoryOut":{"additionalProperties":false,"properties":{"slug":{"title":"Slug","type":"string"},"name":{"title":"Name","type":"string"},"items":{"items":{"$ref":"#/components/schemas/MenuItemOut"},"title":"Items","type":"array"}},"required":["slug","name","items"],"title":"MenuCategoryOut","type":"object"},"MenuItemOut":{"additionalProperties":false,"properties":{"slug":{"title":"Slug","type":"string"},"name":{"title":"Name","type":"string"},"description":{"title":"Description","type":"string"},"chef_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chef Note"},"advisory_note":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Consumer advisory detail for raw/undercooked items (V3.4)","title":"Advisory Note"},"translated":{"title":"Translated","type":"boolean"},"price_cents":{"title":"Price Cents","type":"integer"},"price":{"pattern":"^\\d+\\.\\d{2}$","title":"Price","type":"string"},"calories_kcal":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Calories Kcal"},"nutrition":{"additionalProperties":{"anyOf":[{"type":"number"},{"type":"integer"},{"type":"null"}]},"title":"Nutrition","type":"object"},"allergens":{"items":{"type":"string"},"title":"Allergens","type":"array"},"dietary":{"items":{"enum":["vegetarian","vegan","gluten-free","dairy-free","nut-free","pescatarian"],"type":"string"},"title":"Dietary","type":"array"},"contains_alcohol":{"title":"Contains Alcohol","type":"boolean"},"raw_or_undercooked":{"title":"Raw Or Undercooked","type":"boolean"}},"required":["slug","name","description","chef_note","advisory_note","translated","price_cents","price","calories_kcal","nutrition","allergens","dietary","contains_alcohol","raw_or_undercooked"],"title":"MenuItemOut","type":"object"},"MenuOut":{"additionalProperties":false,"properties":{"locale":{"enum":["en","it","es","fr"],"title":"Locale","type":"string"},"currency":{"const":"USD","title":"Currency","type":"string"},"enrichment_is_synthetic":{"title":"Enrichment Is Synthetic","type":"boolean"},"categories":{"items":{"$ref":"#/components/schemas/MenuCategoryOut"},"title":"Categories","type":"array"},"allergens":{"items":{"$ref":"#/components/schemas/AllergenOut"},"title":"Allergens","type":"array"}},"required":["locale","currency","enrichment_is_synthetic","categories","allergens"],"title":"MenuOut","type":"object"},"MyHistoryChangeOut":{"additionalProperties":false,"properties":{"field":{"title":"Field","type":"string"},"before":{"title":"Before"},"after":{"title":"After"}},"required":["field","before","after"],"title":"MyHistoryChangeOut","type":"object"},"MyHistoryEntryOut":{"additionalProperties":false,"properties":{"id":{"title":"Id","type":"string"},"table":{"enum":["user","customer","reservation"],"title":"Table","type":"string"},"record_id":{"title":"Record Id","type":"integer"},"operation":{"enum":["insert","update","delete"],"title":"Operation","type":"string"},"changed_at":{"title":"Changed At","type":"string"},"actor":{"description":"You · Café Fausse staff · Guest (web form) · a service name","title":"Actor","type":"string"},"actor_kind":{"title":"Actor Kind","type":"string"},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id"},"changes":{"items":{"$ref":"#/components/schemas/MyHistoryChangeOut"},"title":"Changes","type":"array"}},"required":["id","table","record_id","operation","changed_at","actor","actor_kind","request_id","changes"],"title":"MyHistoryEntryOut","type":"object"},"MyHistoryOut":{"additionalProperties":false,"properties":{"total":{"title":"Total","type":"integer"},"page":{"title":"Page","type":"integer"},"page_size":{"title":"Page Size","type":"integer"},"pages":{"title":"Pages","type":"integer"},"entries":{"items":{"$ref":"#/components/schemas/MyHistoryEntryOut"},"title":"Entries","type":"array"}},"required":["total","page","page_size","pages","entries"],"title":"MyHistoryOut","type":"object"},"MyReservationOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"confirmation_code":{"title":"Confirmation Code","type":"string"},"status":{"enum":["booked","seated","completed","no_show","cancelled"],"title":"Status","type":"string"},"time_slot":{"title":"Time Slot","type":"string"},"guests":{"title":"Guests","type":"integer"},"table_number":{"title":"Table Number","type":"integer"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"upcoming":{"title":"Upcoming","type":"boolean"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"cancelled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cancelled At"}},"required":["reservation_id","confirmation_code","status","time_slot","guests","table_number","customer_name","upcoming","created_at","updated_at","cancelled_at"],"title":"MyReservationOut","type":"object"},"MyReservationPatchIn":{"description":"PATCH /me/reservations/{id}: a new time slot and/or party size (at least one) — the manage-by-code rules.","properties":{"time_slot":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"default":null,"title":"Time Slot"},"guests":{"anyOf":[{"maximum":12,"minimum":1,"type":"integer"},{"type":"null"}],"default":null,"title":"Guests"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"title":"MyReservationPatchIn","type":"object"},"MyReservationsOut":{"additionalProperties":false,"properties":{"reservations":{"description":"Every one, newest seating first","items":{"$ref":"#/components/schemas/MyReservationOut"},"title":"Reservations","type":"array"},"upcoming":{"items":{"$ref":"#/components/schemas/MyReservationOut"},"title":"Upcoming","type":"array"},"past":{"items":{"$ref":"#/components/schemas/MyReservationOut"},"title":"Past","type":"array"}},"required":["reservations","upcoming","past"],"title":"MyReservationsOut","type":"object"},"NewsletterIn":{"description":"FR-15: a valid email (and an optional name).","properties":{"email":{"format":"email","maxLength":254,"title":"Email","type":"string"},"customer_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Customer Name"}},"required":["email"],"title":"NewsletterIn","type":"object"},"NewsletterOut":{"additionalProperties":false,"properties":{"subscribed":{"title":"Subscribed","type":"boolean"},"already_subscribed":{"title":"Already Subscribed","type":"boolean"}},"required":["subscribed","already_subscribed"],"title":"NewsletterOut","type":"object"},"NutritionIn":{"additionalProperties":false,"description":"Per serving (21 CFR 101.9 label order); grams to one decimal, the *_mg values whole numbers. Each optional.","properties":{"fat_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Fat G"},"saturated_fat_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Saturated Fat G"},"trans_fat_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Trans Fat G"},"cholesterol_mg":{"anyOf":[{"maximum":100000,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Cholesterol Mg"},"sodium_mg":{"anyOf":[{"maximum":100000,"minimum":0,"type":"integer"},{"type":"null"}],"default":null,"title":"Sodium Mg"},"carbohydrate_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Carbohydrate G"},"fiber_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Fiber G"},"sugars_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Sugars G"},"protein_g":{"anyOf":[{"maximum":9999,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Protein G"}},"title":"NutritionIn","type":"object"},"PageLoadEvent":{"additionalProperties":false,"properties":{"type":{"const":"page_load","title":"Type","type":"string"},"route":{"pattern":"^/[a-z0-9/_-]{0,60}$","title":"Route","type":"string"},"duration_ms":{"anyOf":[{"maximum":600000,"minimum":0,"type":"number"},{"type":"null"}],"default":null,"title":"Duration Ms"}},"required":["type","route"],"title":"PageLoadEvent","type":"object"},"PasswordResetIn":{"properties":{"revoke_password":{"default":false,"title":"Revoke Password","type":"boolean"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"title":"PasswordResetIn","type":"object"},"ProfileOut":{"additionalProperties":false,"properties":{"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The name on the customer record (once the email is verified)","title":"Customer Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"newsletter_signup":{"title":"Newsletter Signup","type":"boolean"},"customer_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"The linked customer record (verified email only)","title":"Customer Id"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"required":["display_name","customer_name","phone_number","newsletter_signup","customer_id","updated_at"],"title":"ProfileOut","type":"object"},"ProfilePatchIn":{"description":"PATCH /me/profile. Absent = keep; \"\" clears the name / phone. ``customer_name`` = alias of ``display_name``.","properties":{"display_name":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"customer_name":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null"}],"default":null,"title":"Customer Name"},"phone_number":{"anyOf":[{"maxLength":25,"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"newsletter_signup":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Newsletter Signup"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"title":"ProfilePatchIn","type":"object"},"ProviderOut":{"additionalProperties":false,"properties":{"id":{"enum":["google","apple","microsoft","linkedin"],"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"login_url":{"title":"Login Url","type":"string"}},"required":["id","name","login_url"],"title":"ProviderOut","type":"object"},"ProvidersOut":{"additionalProperties":false,"properties":{"enabled":{"title":"Enabled","type":"boolean"},"providers":{"items":{"$ref":"#/components/schemas/ProviderOut"},"title":"Providers","type":"array"}},"required":["enabled","providers"],"title":"ProvidersOut","type":"object"},"PublicConfigOut":{"additionalProperties":false,"properties":{"turnstile":{"$ref":"#/components/schemas/TurnstileConfigOut"},"email":{"$ref":"#/components/schemas/FeatureFlagOut"},"sign_in":{"$ref":"#/components/schemas/FeatureFlagOut"},"admin":{"$ref":"#/components/schemas/AdminConfigOut"},"demo":{"default":false,"title":"Demo","type":"boolean"}},"required":["turnstile","email","sign_in","admin"],"title":"PublicConfigOut","type":"object"},"ReadyChecks":{"additionalProperties":false,"properties":{"database":{"enum":["ok","unavailable"],"title":"Database","type":"string"},"procedures":{"enum":["ok","missing","unknown"],"title":"Procedures","type":"string"},"migrations":{"enum":["ok","pending","unknown"],"title":"Migrations","type":"string"},"email":{"anyOf":[{"enum":["smtp","log_only"],"type":"string"},{"type":"null"}],"default":null,"title":"Email"},"turnstile":{"anyOf":[{"enum":["enabled","disabled"],"type":"string"},{"type":"null"}],"default":null,"title":"Turnstile"}},"required":["database","procedures","migrations"],"title":"ReadyChecks","type":"object"},"ReadyOut":{"additionalProperties":false,"properties":{"status":{"enum":["ready","unavailable"],"title":"Status","type":"string"},"checks":{"$ref":"#/components/schemas/ReadyChecks"}},"required":["status","checks"],"title":"ReadyOut","type":"object"},"RegisterIn":{"description":"POST /auth/register: a customer account. ``customer_name`` is accepted as an alias of ``display_name``.","properties":{"email":{"format":"email","maxLength":254,"title":"Email","type":"string"},"password":{"maxLength":1024,"minLength":1,"title":"Password","type":"string"},"display_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"customer_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Customer Name"},"phone_number":{"anyOf":[{"maxLength":25,"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"},"cf-turnstile-response":{"anyOf":[{"maxLength":2048,"type":"string"},{"type":"null"}],"default":null,"title":"Cf-Turnstile-Response"}},"required":["email","password"],"title":"RegisterIn","type":"object"},"ReportDayOut":{"additionalProperties":false,"properties":{"date":{"title":"Date","type":"string"},"reservations":{"title":"Reservations","type":"integer"},"covers":{"title":"Covers","type":"integer"},"cancellations":{"title":"Cancellations","type":"integer"},"newsletter_signups":{"title":"Newsletter Signups","type":"integer"},"seatings":{"title":"Seatings","type":"integer"},"table_capacity":{"title":"Table Capacity","type":"integer"},"table_utilisation":{"title":"Table Utilisation","type":"number"},"busiest_slot_tables":{"title":"Busiest Slot Tables","type":"integer"}},"required":["date","reservations","covers","cancellations","newsletter_signups","seatings","table_capacity","table_utilisation","busiest_slot_tables"],"title":"ReportDayOut","type":"object"},"ReportOut":{"additionalProperties":false,"description":"``totals`` cover the whole period; ``days`` are the grid's page (filters, sort, paging) — every day by\ndefault. ``total`` = days matching the filters.","properties":{"total":{"default":0,"title":"Total","type":"integer"},"page":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page"},"page_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Page Size"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Pages"},"sort":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Sort"},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Q"},"filters":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"title":"Filters"},"period":{"enum":["today","week","range"],"title":"Period","type":"string"},"from":{"title":"From","type":"string"},"to":{"title":"To","type":"string"},"timezone":{"title":"Timezone","type":"string"},"total_tables":{"title":"Total Tables","type":"integer"},"days":{"items":{"$ref":"#/components/schemas/ReportDayOut"},"title":"Days","type":"array"},"totals":{"$ref":"#/components/schemas/ReportTotalsOut"}},"required":["period","from","to","timezone","total_tables","days","totals"],"title":"ReportOut","type":"object"},"ReportTotalsOut":{"additionalProperties":false,"properties":{"reservations":{"title":"Reservations","type":"integer"},"covers":{"title":"Covers","type":"integer"},"cancellations":{"title":"Cancellations","type":"integer"},"newsletter_signups":{"title":"Newsletter Signups","type":"integer"},"table_capacity":{"title":"Table Capacity","type":"integer"},"table_utilisation":{"title":"Table Utilisation","type":"number"}},"required":["reservations","covers","cancellations","newsletter_signups","table_capacity","table_utilisation"],"title":"ReportTotalsOut","type":"object"},"ReservationEditIn":{"additionalProperties":false,"description":"PATCH /admin/reservations/{id}: only the fields sent change. ``phone_number``/``notes`` \"\" clears them.\n``notify`` (default true) emails the guest when the seating, table or party size changed.","properties":{"time_slot":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"default":null,"title":"Time Slot"},"guests":{"anyOf":[{"maximum":12,"minimum":1,"type":"integer"},{"type":"null"}],"default":null,"title":"Guests"},"table_number":{"anyOf":[{"maximum":30,"minimum":1,"type":"integer"},{"type":"null"}],"default":null,"title":"Table Number"},"customer_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Customer Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"notes":{"anyOf":[{"maxLength":500,"type":"string"},{"type":"null"}],"default":null,"title":"Notes"},"notify":{"default":true,"title":"Notify","type":"boolean"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"title":"ReservationEditIn","type":"object"},"ReservationIn":{"description":"FR-6: time slot, number of guests, customer name, email, optional phone (+ newsletter opt-in).","properties":{"time_slot":{"format":"date-time","title":"Time Slot","type":"string"},"guests":{"maximum":12,"minimum":1,"title":"Guests","type":"integer"},"customer_name":{"maxLength":100,"minLength":1,"title":"Customer Name","type":"string"},"email":{"format":"email","maxLength":254,"title":"Email","type":"string"},"phone_number":{"anyOf":[{"maxLength":25,"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"newsletter_signup":{"default":false,"title":"Newsletter Signup","type":"boolean"}},"required":["time_slot","guests","customer_name","email"],"title":"ReservationIn","type":"object"},"ReservationStatusIn":{"additionalProperties":false,"description":"PUT /admin/reservations/{id}/status.","properties":{"status":{"enum":["booked","seated","completed","no_show","cancelled"],"title":"Status","type":"string"},"notify":{"default":true,"title":"Notify","type":"boolean"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"required":["status"],"title":"ReservationStatusIn","type":"object"},"ReservationUpdateIn":{"description":"PATCH /reservations/{code}: the booking's email plus a new time slot and/or party size (at least one).","properties":{"email":{"format":"email","maxLength":254,"title":"Email","type":"string"},"time_slot":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"default":null,"title":"Time Slot"},"guests":{"anyOf":[{"maximum":12,"minimum":1,"type":"integer"},{"type":"null"}],"default":null,"title":"Guests"}},"required":["email"],"title":"ReservationUpdateIn","type":"object"},"ResetPasswordIn":{"description":"POST /auth/password/reset — the emailed link's token and the new password.","properties":{"token":{"maxLength":300,"title":"Token","type":"string"},"password":{"maxLength":1024,"minLength":1,"title":"Password","type":"string"}},"required":["token","password"],"title":"ResetPasswordIn","type":"object"},"RestaurantOut":{"additionalProperties":true,"description":"The public facts of shared/restaurant.json (free-form object; see that file).","properties":{},"title":"RestaurantOut","type":"object"},"SlotOut":{"additionalProperties":false,"properties":{"time_slot":{"pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","title":"Time Slot","type":"string"},"available_tables":{"title":"Available Tables","type":"integer"}},"required":["time_slot","available_tables"],"title":"SlotOut","type":"object"},"StaffAuditOut":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/components/schemas/AuditEventOut"},"title":"Events","type":"array"}},"required":["events"],"title":"StaffAuditOut","type":"object"},"StaffBookingIn":{"additionalProperties":false,"description":"POST /admin/reservations: a phone booking or walk-in taken by staff (same slot rules as the public form).","properties":{"time_slot":{"format":"date-time","title":"Time Slot","type":"string"},"guests":{"maximum":12,"minimum":1,"title":"Guests","type":"integer"},"customer_name":{"maxLength":100,"minLength":1,"title":"Customer Name","type":"string"},"email":{"format":"email","maxLength":254,"title":"Email","type":"string"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Phone Number"},"table_number":{"anyOf":[{"maximum":30,"minimum":1,"type":"integer"},{"type":"null"}],"default":null,"title":"Table Number"},"notes":{"anyOf":[{"maxLength":500,"type":"string"},{"type":"null"}],"default":null,"title":"Notes"},"notify":{"default":true,"title":"Notify","type":"boolean"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"required":["time_slot","guests","customer_name","email"],"title":"StaffBookingIn","type":"object"},"StaffReservationOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"confirmation_code":{"title":"Confirmation Code","type":"string"},"status":{"enum":["booked","cancelled"],"title":"Status","type":"string"},"time_slot":{"title":"Time Slot","type":"string"},"table_number":{"title":"Table Number","type":"integer"},"guests":{"title":"Guests","type":"integer"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"email_masked":{"title":"Email Masked","type":"string"},"created_at":{"title":"Created At","type":"string"},"cancelled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cancelled At"}},"required":["reservation_id","confirmation_code","status","time_slot","table_number","guests","customer_name","email_masked","created_at","cancelled_at"],"title":"StaffReservationOut","type":"object"},"StaffReservationsOut":{"additionalProperties":false,"properties":{"start":{"title":"Start","type":"string"},"end":{"title":"End","type":"string"},"reservations":{"items":{"$ref":"#/components/schemas/StaffReservationOut"},"title":"Reservations","type":"array"}},"required":["start","end","reservations"],"title":"StaffReservationsOut","type":"object"},"StatusOut":{"additionalProperties":false,"properties":{"status":{"title":"Status","type":"string"}},"required":["status"],"title":"StatusOut","type":"object"},"TelemetryBatch":{"additionalProperties":false,"properties":{"events":{"items":{"discriminator":{"mapping":{"api_call":"#/components/schemas/ApiCallEvent","click":"#/components/schemas/ClickEvent","page_load":"#/components/schemas/PageLoadEvent","web_vital":"#/components/schemas/WebVitalEvent"},"propertyName":"type"},"oneOf":[{"$ref":"#/components/schemas/WebVitalEvent"},{"$ref":"#/components/schemas/PageLoadEvent"},{"$ref":"#/components/schemas/ClickEvent"},{"$ref":"#/components/schemas/ApiCallEvent"}]},"maxItems":50,"minItems":1,"title":"Events","type":"array"}},"required":["events"],"title":"TelemetryBatch","type":"object"},"TokenIn":{"description":"POST /auth/verify-email — the token of the emailed link.","properties":{"token":{"maxLength":300,"title":"Token","type":"string"}},"required":["token"],"title":"TokenIn","type":"object"},"TranslationPutIn":{"additionalProperties":false,"description":"PUT /admin/menu/items/{slug}/translations/{locale}.","properties":{"name":{"maxLength":120,"minLength":1,"title":"Name","type":"string"},"description":{"default":"","maxLength":600,"title":"Description","type":"string"},"chef_note":{"anyOf":[{"maxLength":1000,"type":"string"},{"type":"null"}],"default":null,"title":"Chef Note"},"advisory_note":{"anyOf":[{"maxLength":400,"type":"string"},{"type":"null"}],"default":null,"title":"Advisory Note"}},"required":["name"],"title":"TranslationPutIn","type":"object"},"TurnstileConfigOut":{"additionalProperties":false,"properties":{"enabled":{"title":"Enabled","type":"boolean"},"site_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Key"},"field":{"title":"Field","type":"string"},"header":{"title":"Header","type":"string"}},"required":["enabled","site_key","field","header"],"title":"TurnstileConfigOut","type":"object"},"UnsubscribeOut":{"additionalProperties":false,"properties":{"unsubscribed":{"title":"Unsubscribed","type":"boolean"},"already_unsubscribed":{"title":"Already Unsubscribed","type":"boolean"}},"required":["unsubscribed","already_unsubscribed"],"title":"UnsubscribeOut","type":"object"},"UnsubscribeRequestOut":{"additionalProperties":false,"description":"v6: the SAME body whether or not the address is subscribed (no enumeration).","properties":{"accepted":{"title":"Accepted","type":"boolean"},"message":{"title":"Message","type":"string"}},"required":["accepted","message"],"title":"UnsubscribeRequestOut","type":"object"},"UpcomingReservationOut":{"additionalProperties":false,"properties":{"reservation_id":{"title":"Reservation Id","type":"integer"},"confirmation_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Confirmation Code"},"time_slot":{"title":"Time Slot","type":"string"},"guests":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Guests"}},"required":["reservation_id","confirmation_code","time_slot","guests"],"title":"UpcomingReservationOut","type":"object"},"UserCreateIn":{"additionalProperties":false,"description":"POST /admin/users: invite { email, display_name, roles } — the person sets their password from the emailed link.\nA manager may give a ``password`` instead (no email). Roles are checked by the procedure (422 roles: none).","properties":{"email":{"maxLength":254,"title":"Email","type":"string"},"display_name":{"anyOf":[{"maxLength":100,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Display Name"},"roles":{"items":{"type":"string"},"maxItems":5,"title":"Roles","type":"array"},"password":{"anyOf":[{"maxLength":1024,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Password"},"locale":{"default":"en","enum":["en","it","es","fr"],"title":"Locale","type":"string"}},"required":["email","roles"],"title":"UserCreateIn","type":"object"},"UserPatchIn":{"description":"PATCH /admin/users/{id}: the name (If-Match).","properties":{"display_name":{"maxLength":100,"minLength":1,"title":"Display Name","type":"string"}},"required":["display_name"],"title":"UserPatchIn","type":"object"},"UserRolesIn":{"description":"PUT /admin/users/{id}/roles: the user's roles become EXACTLY this set (the union of permissions). Unknown or\nunassignable roles and an empty set are the procedure's 422 roles: none.","properties":{"roles":{"items":{"maxLength":30,"type":"string"},"maxItems":5,"title":"Roles","type":"array"}},"required":["roles"],"title":"UserRolesIn","type":"object"},"UserStatusIn":{"properties":{"status":{"enum":["active","disabled"],"title":"Status","type":"string"}},"required":["status"],"title":"UserStatusIn","type":"object"},"VersionOut":{"additionalProperties":false,"properties":{"name":{"title":"Name","type":"string"},"version":{"title":"Version","type":"string"},"git_sha":{"title":"Git Sha","type":"string"},"build_time":{"title":"Build Time","type":"string"},"schema_version":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Schema Version"},"migration":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Migration"},"schema_head":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Schema Head"},"schema_current":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Schema Current"},"components":{"anyOf":[{"additionalProperties":{"$ref":"#/components/schemas/ComponentVersionOut"},"type":"object"},{"type":"null"}],"default":null,"title":"Components"}},"required":["name","version","git_sha","build_time"],"title":"VersionOut","type":"object"},"WebVitalEvent":{"additionalProperties":false,"properties":{"type":{"const":"web_vital","title":"Type","type":"string"},"name":{"enum":["LCP","INP","CLS","TTFB","FCP"],"title":"Name","type":"string"},"value":{"maximum":600000,"minimum":0,"title":"Value","type":"number"},"route":{"pattern":"^/[a-z0-9/_-]{0,60}$","title":"Route","type":"string"}},"required":["type","name","value","route"],"title":"WebVitalEvent","type":"object"}},"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"__Host-cf_session"},"csrfHeader":{"type":"apiKey","in":"header","name":"X-CSRF-Token"}}}}
