{
  "openapi": "3.1.0",
  "info": {
    "title": "DynBrew API",
    "version": "1.0.0",
    "description": "REST API for DynBrew — recipes, brew sessions with live timers, cellar batches, lab measurements, instruments, webhooks and the Social community. All timestamps are UTC. Success envelope: `{\"ok\":true,\"data\":…}`; paginated lists add `\"meta\":{\"total\",\"limit\",\"offset\"}`; errors are `{\"ok\":false,\"error\":{\"code\",\"message\"}}`. Create an API key on https://web.dynbrew.com/hub/api-keys. Every address a key is called from is recorded, and the owner can block one: any endpoint may then answer 403 with code `ip_blocked` — the key is still valid, the address is not. Blocks are managed at https://web.dynbrew.com/hub/api-key-ips. An MCP server (Model Context Protocol, Streamable HTTP) exposing curated tools over this API is available at https://mcp.dynbrew.com/mcp — authenticate with the same API keys; see https://web.dynbrew.com/hub/docs for setup. A request to an existing path with the wrong HTTP method answers 405 with an Allow header; a body over 8 MB answers 413 — both in the same error envelope.",
    "contact": {
      "name": "DynBrew",
      "url": "https://web.dynbrew.com/hub/docs"
    }
  },
  "servers": [
    {
      "url": "https://api.dynbrew.com"
    }
  ],
  "security": [
    {
      "bearerKey": []
    },
    {
      "queryKey": []
    }
  ],
  "tags": [
    {
      "name": "users"
    },
    {
      "name": "search"
    },
    {
      "name": "library"
    },
    {
      "name": "brewhouse"
    },
    {
      "name": "cellar"
    },
    {
      "name": "lab"
    },
    {
      "name": "social"
    },
    {
      "name": "hub"
    },
    {
      "name": "notifications"
    },
    {
      "name": "i18n"
    },
    {
      "name": "meta"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from /hub/api-keys. Keys carry scopes like brewhouse/*:rw; rw satisfies r. Your keys share a pool of 3600 requests/hour, distributed between them on /hub/api-keys; each key's slice is enforced with X-RateLimit-* headers, 429 + Retry-After when exceeded. Constrained devices may instead POST to /v1/ingest/{api_key}/<path>."
      },
      "queryKey": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key"
      }
    },
    "parameters": {
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 20,
          "minimum": 1,
          "maximum": 100
        }
      },
      "offset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 0,
          "minimum": 0
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The key is valid but the account cannot use the API yet: membership has lapsed (code: membership_expired) or the email address is not confirmed (code: email_unverified). 402 and not 403 on purpose — a 403 here would be indistinguishable from a scope failure.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Insufficient scope (code: forbidden), or the calling address is blocked for this account (code: ip_blocked)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (code: not_found)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Validation": {
        "description": "Validation failed (code: validation_error)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InvalidState": {
        "description": "Operation not allowed in current state (code: invalid_state)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IpBlocked": {
        "description": "The calling address is blocked for this account (code: ip_blocked). Deliberately 403 and not 401: 401 means the key itself is dead, and a client that deletes its key on this would turn a reversible block into a lost credential. The account owner manages blocks at https://web.dynbrew.com/hub/api-key-ips",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (code: rate_limited); see Retry-After",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoContent": {
        "description": "Deleted / done — no body"
      },
      "MethodNotAllowed": {
        "description": "The path exists but not under this HTTP method. The Allow header lists the methods that do.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Request body over 8 MB. The largest legitimate payload (a DBF import) is well under 1 MB.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Meta": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        }
      },
      "IdResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Measure": {
        "type": "object",
        "description": "DBF measure block. Import endpoints also accept a bare number. For addition amounts the unit is honoured: a mass in g, kg, mg, oz or lb (a bare number is kg), or — for a liquid addition — a volume in L, ml, dl or cl, converted to kg at the addition's density (its own `density`, else the catalog's, else 1.0) and echoed back as litres via `display_unit` + `density`; count units such as \"pkg\" are rejected with 422.",
        "properties": {
          "value": {
            "type": "number"
          },
          "unit": {
            "type": "string"
          },
          "qty": {
            "type": "string"
          }
        }
      },
      "DbfAddition": {
        "type": "object",
        "description": "Recipe addition. type/name/amount always present; other fields omitted when empty.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "fermentable",
              "hop",
              "yeast",
              "fining",
              "additive",
              "flavor"
            ]
          },
          "name": {
            "type": "string"
          },
          "amount": {
            "$ref": "#/components/schemas/Measure"
          },
          "time": {
            "$ref": "#/components/schemas/Measure"
          },
          "form": {
            "type": "string"
          },
          "subtype": {
            "type": "string"
          },
          "alpha": {
            "$ref": "#/components/schemas/Measure"
          },
          "beta": {
            "$ref": "#/components/schemas/Measure"
          },
          "color": {
            "$ref": "#/components/schemas/Measure"
          },
          "potential": {
            "$ref": "#/components/schemas/Measure"
          },
          "moisture": {
            "$ref": "#/components/schemas/Measure"
          },
          "attenuation": {
            "$ref": "#/components/schemas/Measure"
          },
          "temp_min": {
            "$ref": "#/components/schemas/Measure"
          },
          "temp_max": {
            "$ref": "#/components/schemas/Measure"
          },
          "lab": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "DbfStep": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "temp": {
            "$ref": "#/components/schemas/Measure"
          },
          "duration": {
            "$ref": "#/components/schemas/Measure"
          },
          "water": {
            "$ref": "#/components/schemas/Measure"
          },
          "description": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "additions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DbfAddition"
            }
          }
        }
      },
      "DbfDocument": {
        "type": "object",
        "description": "DynBrew Format (DBF 1.0) recipe document — full-fidelity recipe representation returned by recipe get/export and accepted by import. Modules other than dbf+recipe are present only when populated. Water minerals are g/L (stored ppm/1000). On IMPORT: addition amounts are mass and the unit is honoured (g, kg, oz, lb; a bare number means kg), and a liquid addition may be a volume (L, ml, dl, cl) converted at its density — the addition then carries `display_unit: \"L\"` and `density` (kg/L) in every document returned — count units such as \"pkg\" are rejected with 422, so send a yeast pack's weight (11 g per dry sachet) instead. Targets are recomputed from the submitted bill by the shared calculator and overwrite any values sent here. Hop alpha, fermentable colour/potential and yeast attenuation are back-filled from the ingredient catalog when omitted and the name matches; send them explicitly to override. carbonation.method is one of forced, priming or spunding — bottle/natural/sugar map to priming and keg/co2 to forced; carbonation.co2 is accepted as an alias for carbonation.level.",
        "required": [
          "dbf",
          "recipe"
        ],
        "properties": {
          "dbf": {
            "type": "string",
            "const": "1.0"
          },
          "recipe": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "style": {
                "type": "string"
              },
              "style_id": {
                "type": "integer"
              },
              "style_ranges": {
                "type": "object",
                "additionalProperties": true
              },
              "batch": {
                "$ref": "#/components/schemas/Measure"
              },
              "efficiency": {
                "$ref": "#/components/schemas/Measure"
              },
              "description": {
                "type": "string"
              },
              "notes": {
                "type": "string"
              }
            }
          },
          "targets": {
            "type": "object",
            "properties": {
              "og": {
                "$ref": "#/components/schemas/Measure"
              },
              "fg": {
                "$ref": "#/components/schemas/Measure"
              },
              "abv": {
                "$ref": "#/components/schemas/Measure"
              },
              "ibu": {
                "$ref": "#/components/schemas/Measure"
              },
              "ebc": {
                "$ref": "#/components/schemas/Measure"
              }
            }
          },
          "water": {
            "type": "object",
            "properties": {
              "volume": {
                "$ref": "#/components/schemas/Measure"
              },
              "source": {
                "type": "object",
                "additionalProperties": true
              },
              "target": {
                "type": "object",
                "additionalProperties": true
              }
            }
          },
          "mash": {
            "type": "object",
            "properties": {
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DbfStep"
                }
              }
            }
          },
          "boil": {
            "$ref": "#/components/schemas/DbfStep"
          },
          "whirlpool": {
            "$ref": "#/components/schemas/DbfStep"
          },
          "fermentation": {
            "type": "object",
            "properties": {
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DbfStep"
                }
              }
            }
          },
          "carbonation": {
            "type": "object",
            "additionalProperties": true
          },
          "volumes": {
            "type": "object",
            "properties": {
              "mash_water": {
                "$ref": "#/components/schemas/Measure"
              },
              "sparge_water": {
                "$ref": "#/components/schemas/Measure"
              },
              "pre_boil": {
                "$ref": "#/components/schemas/Measure"
              },
              "post_boil": {
                "$ref": "#/components/schemas/Measure"
              },
              "fermenter": {
                "$ref": "#/components/schemas/Measure"
              },
              "packaged": {
                "$ref": "#/components/schemas/Measure"
              }
            }
          }
        }
      },
      "RecipeListRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "style_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "batch_volume": {
            "type": [
              "number",
              "null"
            ]
          },
          "efficiency": {
            "type": [
              "number",
              "null"
            ]
          },
          "og_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "abv_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "ibu_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "ebc_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "is_public": {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          },
          "label_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "style_name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Style": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "og_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "og_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "ibu_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "ibu_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "ebc_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "ebc_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "abv_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "abv_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Ingredient": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "fermentable",
              "hop",
              "yeast",
              "fining",
              "additive",
              "flavor"
            ]
          },
          "subtype": {
            "type": [
              "string",
              "null"
            ]
          },
          "producer": {
            "type": [
              "string",
              "null"
            ]
          },
          "alpha": {
            "type": [
              "number",
              "null"
            ]
          },
          "color": {
            "type": [
              "number",
              "null"
            ]
          },
          "potential": {
            "type": [
              "number",
              "null"
            ]
          },
          "attenuation": {
            "type": [
              "number",
              "null"
            ]
          },
          "temp_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "temp_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "planning",
              "brewing",
              "transferred",
              "completed",
              "aborted"
            ]
          },
          "brew_date": {
            "type": [
              "string",
              "null"
            ]
          },
          "batch_volume": {
            "type": [
              "number",
              "null"
            ]
          },
          "og_target": {
            "type": [
              "number",
              "null"
            ]
          },
          "og_actual": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg_actual": {
            "type": [
              "number",
              "null"
            ]
          },
          "recipe_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "recipe_snapshot": {
            "type": [
              "object",
              "null"
            ],
            "description": "DBF document snapshot (full sessionGet only)"
          },
          "label_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "recipe_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SessionStep"
            },
            "description": "Present on sessionGet only"
          },
          "stage_loggers": {
            "type": "array",
            "description": "Logger attached per stage (see /stage-loggers)",
            "items": {
              "type": "object",
              "properties": {
                "stage": {
                  "type": "string",
                  "enum": [
                    "prep",
                    "mash",
                    "sparge",
                    "boil",
                    "whirlpool",
                    "chill",
                    "fermentation",
                    "carbonation",
                    "other"
                  ]
                },
                "logger_id": {
                  "type": "integer"
                },
                "logger_name": {
                  "type": "string"
                },
                "logger_type": {
                  "type": "string",
                  "enum": [
                    "gravity",
                    "ph",
                    "temperature",
                    "pressure",
                    "brix",
                    "volume",
                    "other"
                  ]
                },
                "unit": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "SessionStep": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "session_id": {
            "type": "integer"
          },
          "stage": {
            "type": "string",
            "enum": [
              "prep",
              "mash",
              "sparge",
              "boil",
              "whirlpool",
              "chill",
              "fermentation",
              "carbonation",
              "other"
            ]
          },
          "name": {
            "type": "string"
          },
          "sort_order": {
            "type": "integer"
          },
          "target_pressure": {
            "type": [
              "number",
              "null"
            ],
            "description": "Bar (gauge) — pressure-fermentation target on fermentation steps"
          },
          "target_temp": {
            "type": [
              "number",
              "null"
            ]
          },
          "target_duration": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Minutes. >= 1440 marks a long (day-scale) step"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "actual_temp": {
            "type": [
              "number",
              "null"
            ]
          },
          "actual_duration": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Minutes"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "timer_fired": {
            "type": [
              "string",
              "null"
            ]
          },
          "next_timer_event_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "carbonation": {
            "type": [
              "object",
              "null"
            ],
            "description": "Carbonation steps only. The shared carbonation model's reading of this step: elapsed days, estimated dissolved CO2 and the instant the beer is called ready. Null when the plan has no carbonation target.",
            "properties": {
              "method": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "priming",
                  "forced",
                  "spunding",
                  null
                ]
              },
              "target_vols": {
                "type": "number",
                "description": "Target volumes of CO2 from the plan"
              },
              "temp": {
                "type": "number",
                "description": "Conditioning temperature used, °C (step target or the method's default)"
              },
              "span_days": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Days the model gives from start to ready; null when the method has no clock (spunding)"
              },
              "days": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Days elapsed since the step started (frozen at completion); null before start"
              },
              "est_vols": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Estimated dissolved CO2 now, volumes — a model reading, not a measurement"
              },
              "pct": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Progress toward target, 0-100"
              },
              "ready_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "ready": {
                "type": "boolean",
                "description": "True once ready_at has passed on a still-running step"
              },
              "done": {
                "type": "boolean",
                "description": "Step completed"
              }
            }
          }
        }
      },
      "Timer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "session_<sid>_step_<stepid>"
          },
          "session_id": {
            "type": "integer"
          },
          "session_name": {
            "type": "string"
          },
          "step_id": {
            "type": "integer"
          },
          "step_name": {
            "type": "string"
          },
          "stage": {
            "type": "string"
          },
          "started_at": {
            "type": "string"
          },
          "duration_min": {
            "type": "integer"
          },
          "elapsed_sec": {
            "type": "integer"
          },
          "remaining_sec": {
            "type": "integer",
            "description": "May be negative on overrun"
          },
          "done_fired": {
            "type": "boolean"
          },
          "checkpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "amount": {
                  "type": "number"
                },
                "time": {
                  "type": "integer"
                },
                "elapsed_at": {
                  "type": "integer"
                },
                "remaining": {
                  "type": "integer"
                },
                "fired": {
                  "type": "boolean"
                },
                "warned": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "SessionLogEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "type": {
            "type": "string",
            "enum": [
              "measurement",
              "note",
              "event",
              "sensor"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "logged_at": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "Batch": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "og": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg": {
            "type": [
              "number",
              "null"
            ]
          },
          "abv": {
            "type": [
              "number",
              "null"
            ]
          },
          "volume": {
            "type": [
              "number",
              "null"
            ]
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "packaged_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "best_before": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "label_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "session_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "inventory": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InventoryItem"
            },
            "description": "Present on batchGet only"
          }
        }
      },
      "StockItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "ingredient_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "fermentable",
              "hop",
              "yeast",
              "fining",
              "additive",
              "flavor"
            ]
          },
          "subtype": {
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "type": "number"
          },
          "purchased_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "InventoryItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "batch_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "keg",
              "bottle",
              "can",
              "other"
            ]
          },
          "volume": {
            "type": "number"
          },
          "quantity": {
            "type": "number"
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "batch_name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Review": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "subject_type": {
            "type": "string",
            "enum": [
              "lib_ingredient",
              "place",
              "place_product"
            ]
          },
          "subject_id": {
            "type": "integer"
          },
          "session_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "batch_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rating": {
            "type": "number",
            "minimum": 1,
            "maximum": 5
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "body": {
            "type": [
              "string",
              "null"
            ]
          },
          "recommend": {
            "type": "string",
            "enum": [
              "yes",
              "no",
              "neutral"
            ]
          },
          "is_hidden": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Measurement": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "logger_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "session_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "batch_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "instrument_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "type": {
            "type": "string"
          },
          "value": {
            "type": "number"
          },
          "unit": {
            "type": "string"
          },
          "qty": {
            "type": [
              "number",
              "null"
            ]
          },
          "measured_at": {
            "type": "string"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "span_started_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Start of the run this row covers when idle repeats were folded into it (see POST description); null for a single reading."
          },
          "sample_count": {
            "type": "integer",
            "description": "Readings folded into this row; 1 for a single reading. measured_at is always the LAST reading of the run."
          }
        }
      },
      "Logger": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "instrument_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "EquipmentProfile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rate_limit": {
            "type": "integer",
            "description": "This key's slice of the account's shared pool of 3600 calls/hour."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_used_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "Address the key was last seen from. Null for keys unused since this was first recorded."
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "Instrument": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "make": {
            "type": [
              "string",
              "null"
            ]
          },
          "manufacturer": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ]
          },
          "template": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_filter": {
            "type": [
              "object",
              "array",
              "null"
            ],
            "description": "Glob patterns, e.g. brewhouse.*, *.alarm. Known events: batch.timer, batch.status, instrument.alarm, dm.received, webhook.test, webhook.incoming"
          },
          "severity_filter": {
            "type": [
              "object",
              "array",
              "null"
            ],
            "description": "info | warning | critical"
          },
          "enabled": {
            "type": "integer"
          },
          "incoming_token": {
            "type": [
              "string",
              "null"
            ]
          },
          "incoming_action": {
            "type": [
              "string",
              "null"
            ]
          },
          "incoming_config": {
            "type": [
              "object",
              "null"
            ]
          },
          "last_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_sent_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Notification": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "source": {
            "type": "string"
          },
          "event": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": [
              "string",
              "null"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "ref_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "ref_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "read_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "FeedEvent": {
        "type": "object",
        "description": "Heterogeneous feed entry; fields beyond the common set depend on type.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "tap_added",
              "tasting_note",
              "session_completed"
            ]
          },
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "batch_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "batch_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "session_name": {
            "type": "string"
          },
          "rating": {
            "type": [
              "number",
              "null"
            ]
          },
          "occurred_at": {
            "type": "string"
          }
        }
      },
      "TapEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "batch_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "is_active": {
            "type": "integer"
          },
          "tapped_at": {
            "type": "string"
          },
          "batch_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "label_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "abv": {
            "type": [
              "number",
              "null"
            ]
          },
          "og": {
            "type": [
              "number",
              "null"
            ]
          },
          "fg": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "TastingNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "batch_id": {
            "type": "integer"
          },
          "appearance": {
            "type": [
              "string",
              "null"
            ]
          },
          "aroma": {
            "type": [
              "string",
              "null"
            ]
          },
          "taste": {
            "type": [
              "string",
              "null"
            ]
          },
          "mouthfeel": {
            "type": [
              "string",
              "null"
            ]
          },
          "overall": {
            "type": [
              "string",
              "null"
            ]
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 5
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "batch_name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "FollowEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Relationship row id"
          },
          "user_id": {
            "type": "integer",
            "description": "The other user's id"
          },
          "username": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "mutual": {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          }
        }
      },
      "Place": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "is_public": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string"
          },
          "member_count": {
            "type": "integer"
          }
        }
      },
      "ChatChannel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "place_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "position": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "ChatMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "channel_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "edited_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DmThread": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "other_id": {
            "type": "integer"
          },
          "other_name": {
            "type": "string"
          },
          "last_message_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_body": {
            "type": [
              "string",
              "null"
            ]
          },
          "unread_count": {
            "type": "integer"
          }
        }
      },
      "DmMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "thread_id": {
            "type": "integer"
          },
          "sender_id": {
            "type": "integer"
          },
          "body": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "read_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SocialEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "host_place_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "user_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "starts_at": {
            "type": "string"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "label_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "is_public": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "host_place_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "host_username": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "UserMe": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_trusted": {
            "type": "integer"
          },
          "is_admin": {
            "type": "integer"
          },
          "timer_warn_minutes": {
            "type": "integer"
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "profile": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Present only for a full-access key. Omitted entirely for a scoped key, which gets identity and membership but not the account."
          }
        }
      },
      "UserPublic": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "username": {
            "type": "string"
          },
          "is_trusted": {
            "type": "integer"
          },
          "is_admin": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "WaterProfile": {
        "type": "object",
        "description": "Minerals in ppm (mg/L). user_id null = global preset.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "purpose": {
            "type": "string",
            "enum": [
              "source",
              "target"
            ]
          },
          "ca": {
            "type": [
              "number",
              "null"
            ]
          },
          "mg": {
            "type": [
              "number",
              "null"
            ]
          },
          "na": {
            "type": [
              "number",
              "null"
            ]
          },
          "so4": {
            "type": [
              "number",
              "null"
            ]
          },
          "cl": {
            "type": [
              "number",
              "null"
            ]
          },
          "hco3": {
            "type": [
              "number",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Supplier": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "cover_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_featured": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SupplierProduct"
            },
            "description": "supplierGet only"
          }
        }
      },
      "SupplierProduct": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "supplier_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "fermentable",
              "hop",
              "yeast",
              "ingredient-other",
              "equipment-mash",
              "equipment-boil",
              "equipment-ferment",
              "equipment-kegging",
              "equipment-cooling",
              "equipment-measurement",
              "equipment-cleaning",
              "books-media",
              "other"
            ]
          },
          "price": {
            "type": [
              "number",
              "null"
            ]
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "external_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "SocialProfile": {
        "type": "object",
        "description": "Only fields the viewer may see are present (self > mutual/'regulars' > public).",
        "properties": {
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "username": {
                "type": "string"
              },
              "created_at": {
                "type": "string"
              }
            }
          },
          "relationship": {
            "type": "string",
            "enum": [
              "self",
              "mutual",
              "other"
            ]
          },
          "profile": {
            "type": "object",
            "properties": {
              "display_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "brewery_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "avatar": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Path under web.dynbrew.com"
              },
              "bio": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "brewer_since": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "experience_level": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "beginner",
                  "intermediate",
                  "advanced",
                  "expert",
                  null
                ]
              },
              "website": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "external_links": {
                "type": [
                  "array",
                  "object",
                  "null"
                ]
              }
            }
          }
        }
      },
      "Article": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "body": {
            "type": "string",
            "description": "Markdown; articleGet only"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_public": {
            "type": "integer"
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "GlossaryTerm": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "term": {
            "type": "string"
          },
          "definition": {
            "type": "string"
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_public": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "Technique": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "body": {
            "type": "string",
            "description": "Markdown; techniqueGet only"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "difficulty": {
            "type": [
              "string",
              "null"
            ]
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_public": {
            "type": "integer"
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "CalendarEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "maxLength": 160
          },
          "kind": {
            "type": "string",
            "enum": [
              "brew",
              "clean",
              "maintenance",
              "purchase",
              "other"
            ]
          },
          "color": {
            "type": "string",
            "nullable": true,
            "description": "#rrggbb hex; null = the kind's default color"
          },
          "equipment_id": {
            "type": "integer",
            "nullable": true,
            "description": "One of your equipment profiles reserved for the period"
          },
          "equipment_name": {
            "type": "string",
            "nullable": true
          },
          "starts_on": {
            "type": "string",
            "format": "date"
          },
          "ends_on": {
            "type": "string",
            "format": "date"
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "paths": {
    "/v1/health": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "Health probe (no auth, no rate limit)",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          },
          "503": {
            "description": "Database unreachable"
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This OpenAPI document (no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 JSON"
          }
        }
      }
    },
    "/v1/i18n/{locale}": {
      "get": {
        "tags": [
          "i18n"
        ],
        "summary": "UI translation catalogue (ETag/304 supported)",
        "parameters": [
          {
            "name": "locale",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "locale": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        },
                        "strings": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified (If-None-Match)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/sync": {
      "get": {
        "tags": [
          "sync"
        ],
        "summary": "Combined poll: notifications, unread DMs, active loggers, brew timers, watched steps and idle brews in one call (scope-gated per section)",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO 8601 UTC cursor — notification items are limited to those created after it. Feed the response's `time` back as the next `since`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sync state. Sections the key's scopes cannot read are listed in `skipped` instead of failing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "notifications": {
                      "type": "object",
                      "properties": {
                        "unread": {
                          "type": "integer"
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "source": {
                                "type": "string"
                              },
                              "event": {
                                "type": "string"
                              },
                              "title": {
                                "type": "string"
                              },
                              "body": {
                                "type": "string"
                              },
                              "severity": {
                                "type": "string"
                              },
                              "ref_type": {
                                "type": "string"
                              },
                              "ref_id": {
                                "type": "integer"
                              },
                              "read_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    },
                    "messages": {
                      "type": "object",
                      "properties": {
                        "unread": {
                          "type": "integer"
                        },
                        "threads": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "other_id": {
                                "type": "integer"
                              },
                              "other_name": {
                                "type": "string"
                              },
                              "last_message_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "unread_count": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    },
                    "loggers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "unit": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "last_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "last_value": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "timers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "session_id": {
                            "type": "integer"
                          },
                          "session_name": {
                            "type": "string"
                          },
                          "step_id": {
                            "type": "integer"
                          },
                          "step_name": {
                            "type": "string"
                          },
                          "stage": {
                            "type": "string"
                          },
                          "started_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "target_duration": {
                            "type": "integer",
                            "description": "minutes"
                          },
                          "next_timer_event_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "description": "Running steps with a duration, soonest to finish first. Steps without a duration are never timers — see `watched`."
                    },
                    "watched": {
                      "type": "array",
                      "description": "Running steps without a duration (carbonation), watched rather than counted down. Carries the same carbonation reading as the session detail's step.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "session_id": {
                            "type": "integer"
                          },
                          "session_name": {
                            "type": "string"
                          },
                          "label_path": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "step_id": {
                            "type": "integer"
                          },
                          "step_name": {
                            "type": "string"
                          },
                          "stage": {
                            "type": "string"
                          },
                          "started_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "carbonation": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "Carbonation steps only. The shared carbonation model's reading of this step: elapsed days, estimated dissolved CO2 and the instant the beer is called ready. Null when the plan has no carbonation target.",
                            "properties": {
                              "method": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "enum": [
                                  "priming",
                                  "forced",
                                  "spunding",
                                  null
                                ]
                              },
                              "target_vols": {
                                "type": "number",
                                "description": "Target volumes of CO2 from the plan"
                              },
                              "temp": {
                                "type": "number",
                                "description": "Conditioning temperature used, °C (step target or the method's default)"
                              },
                              "span_days": {
                                "type": [
                                  "number",
                                  "null"
                                ],
                                "description": "Days the model gives from start to ready; null when the method has no clock (spunding)"
                              },
                              "days": {
                                "type": [
                                  "number",
                                  "null"
                                ],
                                "description": "Days elapsed since the step started (frozen at completion); null before start"
                              },
                              "est_vols": {
                                "type": [
                                  "number",
                                  "null"
                                ],
                                "description": "Estimated dissolved CO2 now, volumes — a model reading, not a measurement"
                              },
                              "pct": {
                                "type": [
                                  "integer",
                                  "null"
                                ],
                                "description": "Progress toward target, 0-100"
                              },
                              "ready_at": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time"
                              },
                              "ready": {
                                "type": "boolean",
                                "description": "True once ready_at has passed on a still-running step"
                              },
                              "done": {
                                "type": "boolean",
                                "description": "Step completed"
                              }
                            }
                          }
                        }
                      }
                    },
                    "idle": {
                      "type": "array",
                      "description": "Brewing sessions with no running step right now — just started, or between two steps (mash done, sparge not yet started). Listed so a live view keeps showing the brew while nothing is counting down.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "session_id": {
                            "type": "integer"
                          },
                          "session_name": {
                            "type": "string"
                          },
                          "label_path": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "next_step_id": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "First unstarted step in plan order; null when every step is done"
                          },
                          "next_step_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "next_stage": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_done_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the most recent step was completed; null when none is yet"
                          },
                          "idle_since": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the wait began: last_done_at, or the start of the brew day when no step is done"
                          }
                        }
                      }
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/test": {
      "get": {
        "tags": [
          "test"
        ],
        "summary": "Request inspector — echoes the request and returns your key's canned response",
        "description": "Diagnostic sandbox tied to the calling API key. The request (method, query, headers, body) is captured and shown live on the Hub → API Keys page; the response is whatever canned status + JSON/text body that key is configured with, or an echo of the request when unconfigured. Any valid key works; no scope required.",
        "responses": {
          "200": {
            "description": "The key's canned response, or an echo envelope when no response is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "test"
        ],
        "summary": "Request inspector — send form-data or JSON; echoed and answered with your canned response",
        "description": "Diagnostic sandbox tied to the calling API key. The request (method, query, headers, body) is captured and shown live on the Hub → API Keys page; the response is whatever canned status + JSON/text body that key is configured with, or an echo of the request when unconfigured. Any valid key works; no scope required.",
        "responses": {
          "200": {
            "description": "The key's canned response, or an echo envelope when no response is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "test"
        ],
        "summary": "Request inspector — PUT variant of the echo/test endpoint",
        "description": "Diagnostic sandbox tied to the calling API key. The request (method, query, headers, body) is captured and shown live on the Hub → API Keys page; the response is whatever canned status + JSON/text body that key is configured with, or an echo of the request when unconfigured. Any valid key works; no scope required.",
        "responses": {
          "200": {
            "description": "The key's canned response, or an echo envelope when no response is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "test"
        ],
        "summary": "Request inspector — PATCH variant of the echo/test endpoint",
        "description": "Diagnostic sandbox tied to the calling API key. The request (method, query, headers, body) is captured and shown live on the Hub → API Keys page; the response is whatever canned status + JSON/text body that key is configured with, or an echo of the request when unconfigured. Any valid key works; no scope required.",
        "responses": {
          "200": {
            "description": "The key's canned response, or an echo envelope when no response is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "test"
        ],
        "summary": "Request inspector — DELETE variant of the echo/test endpoint",
        "description": "Diagnostic sandbox tied to the calling API key. The request (method, query, headers, body) is captured and shown live on the Hub → API Keys page; the response is whatever canned status + JSON/text body that key is configured with, or an echo of the request when unconfigured. Any valid key works; no scope required.",
        "responses": {
          "200": {
            "description": "The key's canned response, or an echo envelope when no response is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/me": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Current user (scope *:r)",
        "responses": {
          "200": {
            "description": "User",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/UserMe"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "users"
        ],
        "summary": "Update current user (scope *:rw)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "timer_warn_minutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 60
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/users/{id}": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Public user profile (scope *:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/UserPublic"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/search/q/{term}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "Global search: users + recipes + styles (scope *:r)",
        "parameters": [
          {
            "name": "term",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped matches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "users": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "recipes": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "styles": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/q/{term}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "Search users by username (scope *:r)",
        "parameters": [
          {
            "name": "term",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated users {id, username}"
          }
        }
      }
    },
    "/v1/library/recipes/q/{term}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "Search recipes by name (scope library/*:r)",
        "parameters": [
          {
            "name": "term",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated recipe rows"
          }
        }
      }
    },
    "/v1/library/recipes/by-style/{style_id}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "Recipes in a style (scope library/*:r)",
        "parameters": [
          {
            "name": "style_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated recipe rows"
          }
        }
      }
    },
    "/v1/library/recipes/by-user/{user_id}": {
      "get": {
        "tags": [
          "search"
        ],
        "summary": "A user's recipes — public only unless self (scope library/*:r)",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated recipe rows"
          }
        }
      }
    },
    "/v1/library/recipes": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List recipes — own + public (scope library/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Name contains"
          },
          {
            "name": "is_public",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                0,
                1
              ]
            }
          },
          {
            "name": "style_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "og_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "og_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "fg_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "fg_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "abv_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "abv_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "ibu_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "ibu_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "ebc_min",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "ebc_max",
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "hop",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Has hop matching name"
          },
          {
            "name": "yeast",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fermentable",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "created",
                "og_target",
                "ibu_target"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecipeListRow"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Create recipe (metadata; use import for full DBF) (scope library/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "batch_volume": {
                    "type": "number"
                  },
                  "efficiency": {
                    "type": "number",
                    "default": 0.72
                  },
                  "style_id": {
                    "type": "integer"
                  },
                  "og_target": {
                    "type": "number"
                  },
                  "fg_target": {
                    "type": "number"
                  },
                  "abv_target": {
                    "type": "number"
                  },
                  "ibu_target": {
                    "type": "number"
                  },
                  "ebc_target": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "is_public": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/recipes/import": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Import a full DBF recipe document (scope library/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DbfDocument"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/recipes/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get recipe as full DBF document (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "DBF",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/DbfDocument"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Update recipe metadata (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "style_id": {
                    "type": "integer"
                  },
                  "batch_volume": {
                    "type": "number"
                  },
                  "efficiency": {
                    "type": "number"
                  },
                  "og_target": {
                    "type": "number"
                  },
                  "fg_target": {
                    "type": "number"
                  },
                  "abv_target": {
                    "type": "number"
                  },
                  "ibu_target": {
                    "type": "number"
                  },
                  "ebc_target": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "is_public": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "library"
        ],
        "summary": "Merge a partial DBF into an existing recipe (scope library/*:rw)",
        "description": "Partial update. Send only the DBF modules that change: a module you omit is left untouched, a module you include replaces that module whole (steps and additions are not merged element by element), and a module set to null is removed. The `recipe` header is the exception and merges field by field. OG/FG/ABV/IBU/EBC are always recomputed from the merged document via the shared calculator, never taken from the request. The pre-edit state is versioned automatically. Responds with the full updated DBF document so the caller can read the recomputed targets.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A partial DBF document — any subset of `recipe`, `prep`, `mash`, `sparge`, `boil`, `whirlpool`, `fermentation`, `carbonation`, `water`, `volumes`.",
                "properties": {
                  "recipe": {
                    "type": "object",
                    "description": "Header fields; merged field by field."
                  },
                  "mash": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "boil": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "water": {
                    "type": [
                      "object",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The full updated recipe as a DBF document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/DbfDocument"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "library"
        ],
        "summary": "Delete recipe (soft) (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/recipes/{id}/clone": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Deep-copy a recipe (own or public) (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "New recipe id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/recipes/{id}/export": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Export recipe as DBF (alias of GET) (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "DBF document"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/styles": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List beer styles (scope library/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Name or category contains"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Style"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Create style (admin only) (scope library/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "og_min": {
                    "type": "number"
                  },
                  "og_max": {
                    "type": "number"
                  },
                  "fg_min": {
                    "type": "number"
                  },
                  "fg_max": {
                    "type": "number"
                  },
                  "ibu_min": {
                    "type": "number"
                  },
                  "ibu_max": {
                    "type": "number"
                  },
                  "ebc_min": {
                    "type": "number"
                  },
                  "ebc_max": {
                    "type": "number"
                  },
                  "abv_min": {
                    "type": "number"
                  },
                  "abv_max": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/styles/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get style (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Style",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Style"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Update style (admin only) (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/ingredients": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List all ingredients (scope library/ingredients/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subtype",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "producer",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ingredient"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/library/ingredients/{type}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List ingredients of a type (scope library/ingredients/{type}:r)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subtype",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "producer",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated ingredients"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Create ingredient (admin only) (scope library/ingredients/{type}:rw)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "subtype": {
                    "type": "string"
                  },
                  "producer": {
                    "type": "string"
                  },
                  "alpha": {
                    "type": "number"
                  },
                  "color": {
                    "type": "number"
                  },
                  "potential": {
                    "type": "number"
                  },
                  "attenuation": {
                    "type": "number"
                  },
                  "temp_min": {
                    "type": "number"
                  },
                  "temp_max": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/ingredients/{type}/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get ingredient (scope library/ingredients/{type}:r)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ingredient",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Ingredient"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Update ingredient (admin only) (scope library/ingredients/{type}:rw)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/calendar": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "List calendar activities — planned brew days, cleaning, maintenance (scope brewhouse/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only activities overlapping the window from this date"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only activities overlapping the window up to this date"
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "brew",
                "clean",
                "maintenance",
                "purchase",
                "other"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CalendarEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Create calendar activity (scope brewhouse/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "starts_on"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "brew",
                      "clean",
                      "maintenance",
                      "purchase",
                      "other"
                    ],
                    "default": "other"
                  },
                  "color": {
                    "type": "string",
                    "description": "#rrggbb hex; omit for the kind's default"
                  },
                  "equipment_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "starts_on": {
                    "type": "string",
                    "format": "date"
                  },
                  "ends_on": {
                    "type": "string",
                    "format": "date",
                    "description": "Defaults to starts_on"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/brewhouse/calendar/{id}": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Get calendar activity (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/CalendarEvent"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Update calendar activity — partial body accepted (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "starts_on"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "brew",
                      "clean",
                      "maintenance",
                      "purchase",
                      "other"
                    ],
                    "default": "other"
                  },
                  "color": {
                    "type": "string",
                    "description": "#rrggbb hex; omit for the kind's default"
                  },
                  "equipment_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "starts_on": {
                    "type": "string",
                    "format": "date"
                  },
                  "ends_on": {
                    "type": "string",
                    "format": "date",
                    "description": "Defaults to starts_on"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Delete calendar activity (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/brewhouse/sessions": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "List brew sessions (scope brewhouse/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "planning",
                "brewing",
                "transferred",
                "completed",
                "aborted"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "started_at",
                "name"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Session"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Create session — optionally from a recipe (copies steps + DBF snapshot; scales if batch_volume differs) (scope brewhouse/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "recipe_id": {
                    "type": "integer"
                  },
                  "brew_date": {
                    "type": "string"
                  },
                  "batch_volume": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Get session incl. steps and recipe snapshot (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Session"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Update session fields (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "brew_date": {
                    "type": "string"
                  },
                  "batch_volume": {
                    "type": "number"
                  },
                  "og_target": {
                    "type": "number"
                  },
                  "og_actual": {
                    "type": "number"
                  },
                  "fg_actual": {
                    "type": "number"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Delete session (soft) (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/start": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Start brew day: planning → brewing (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, status: brewing}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/complete": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Complete session: brewing → completed; auto-creates a cellar batch (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, status: completed, cellar_batch_id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/reopen": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "HARD reset completed/aborted → planning. Deletes spawned batches, measurements, log, tap entries. Prefer /rewind for partial corrections. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, status: planning}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/rewind": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Rewind progress to a step: clears the step and everything after it (sort_order), keeps earlier progress. Reopens finished sessions to brewing. Logged to the session log. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "step_id"
                ],
                "properties": {
                  "step_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, status: brewing, rewound_to, steps_reset}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/scale": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Scale batch volume; recipe snapshot amounts scale proportionally (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "batch_volume"
                ],
                "properties": {
                  "batch_volume": {
                    "type": "number",
                    "exclusiveMinimum": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated session row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/abort": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Abort a planning/brewing session (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, status: aborted}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/log": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Session log entries (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SessionLogEntry"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Append log entry (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "data"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "measurement",
                      "note",
                      "event",
                      "sensor"
                    ]
                  },
                  "data": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "logged_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/timers": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "All running step timers with hop-addition checkpoints, plus brewing sessions idle between steps (scope brewhouse/*:r)",
        "responses": {
          "200": {
            "description": "Timers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "timers": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Timer"
                          }
                        },
                        "idle": {
                          "type": "array",
                          "description": "Brewing sessions with no running step right now — just started, or between two steps (mash done, sparge not yet started). Listed so a live view keeps showing the brew while nothing is counting down.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "session_id": {
                                "type": "integer"
                              },
                              "session_name": {
                                "type": "string"
                              },
                              "label_path": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "next_step_id": {
                                "type": [
                                  "integer",
                                  "null"
                                ],
                                "description": "First unstarted step in plan order; null when every step is done"
                              },
                              "next_step_name": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "next_stage": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "last_done_at": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time",
                                "description": "When the most recent step was completed; null when none is yet"
                              },
                              "idle_since": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the wait began: last_done_at, or the start of the brew day when no step is done"
                              }
                            }
                          }
                        },
                        "warn_minutes": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Add a step (planning sessions only) (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "stage": {
                    "type": "string",
                    "enum": [
                      "mash",
                      "boil",
                      "whirlpool",
                      "chill",
                      "other"
                    ],
                    "default": "other"
                  },
                  "target_pressure": {
                    "type": "number",
                    "description": "Bar (gauge) — pressure-fermentation target"
                  },
                  "target_temp": {
                    "type": "number"
                  },
                  "target_duration": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Minutes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created step row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/SessionStep"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps/{step_id}": {
      "patch": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Update step: notes/name/targets any time; started_at+completed_at (UTC pair) retro-edits times on a COMPLETED step and recomputes actual_duration (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "target_pressure": {
                    "type": "number",
                    "description": "Bar (gauge) — pressure-fermentation target"
                  },
                  "target_temp": {
                    "type": "number"
                  },
                  "target_duration": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "started_at": {
                    "type": "string",
                    "description": "UTC 'YYYY-MM-DD HH:MM[:SS]' — requires completed_at too"
                  },
                  "completed_at": {
                    "type": "string",
                    "description": "UTC; must be after started_at"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated step row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Delete a step (planning sessions only) (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps/{step_id}/start": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Start a step timer; minutes_ago backdates the start (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minutes_ago": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Step row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps/{step_id}/adjust": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Correct a running step's start time to now − minutes_ago (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "minutes_ago"
                ],
                "properties": {
                  "minutes_ago": {
                    "type": "integer",
                    "exclusiveMinimum": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Step row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps/{step_id}/stop": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Un-start a running step (clears its timer) (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Step row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/steps/{step_id}/complete": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Complete a step. Explicit started_at+completed_at (UTC pair) take precedence; else actual_duration reconciles the timestamps. For day-scale steps (target >= 1440 min) actual_duration is DAYS. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "actual_duration": {
                    "type": "integer",
                    "description": "Minutes (days when target_duration >= 1440)"
                  },
                  "actual_temp": {
                    "type": "number",
                    "description": "Defaults to target_temp"
                  },
                  "started_at": {
                    "type": "string",
                    "description": "UTC 'YYYY-MM-DD HH:MM[:SS]' — pair with completed_at"
                  },
                  "completed_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Step row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/InvalidState"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/cellar/batches": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "List batches (scope cellar/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "started_at",
                "best_before",
                "name"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Batch"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Create batch (scope cellar/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "session_id": {
                    "type": "integer"
                  },
                  "og": {
                    "type": "number"
                  },
                  "fg": {
                    "type": "number"
                  },
                  "abv": {
                    "type": "number"
                  },
                  "volume": {
                    "type": "number"
                  },
                  "started_at": {
                    "type": "string"
                  },
                  "packaged_at": {
                    "type": "string"
                  },
                  "best_before": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/cellar/batches/{id}": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "Get batch incl. its inventory (scope cellar/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Batch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Batch"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "cellar"
        ],
        "summary": "Update batch. If fg given without abv, abv is recomputed. (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "archived"
                    ]
                  },
                  "og": {
                    "type": "number"
                  },
                  "fg": {
                    "type": "number"
                  },
                  "abv": {
                    "type": "number"
                  },
                  "volume": {
                    "type": "number"
                  },
                  "started_at": {
                    "type": "string"
                  },
                  "packaged_at": {
                    "type": "string"
                  },
                  "best_before": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "cellar"
        ],
        "summary": "Delete batch (soft) (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/cellar/stock": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "List all raw-ingredient stock (scope cellar/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expires_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/cellar/stock/cleanup": {
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Bulk stock cleanup — quick delete by criteria (scope cellar/*:rw)",
        "description": "Soft-deletes stock rows matching one criterion: expired (past expiry date), empty (zero amount), stale (untouched for `months` months — 3, 6 or 12), or shopping (clears the whole shopping list: rows that only existed as shopping entries are deleted, the rest just lose the flag). Every criterion except shopping skips rows on the shopping list.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "criteria"
                ],
                "properties": {
                  "criteria": {
                    "type": "string",
                    "enum": [
                      "expired",
                      "empty",
                      "stale",
                      "shopping"
                    ]
                  },
                  "months": {
                    "type": "integer",
                    "enum": [
                      3,
                      6,
                      12
                    ],
                    "description": "Required when criteria is stale."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "criteria": {
                          "type": "string"
                        },
                        "deleted": {
                          "type": "integer",
                          "description": "Rows soft-deleted."
                        },
                        "cleared": {
                          "type": "integer",
                          "description": "Rows that only lost the shopping flag (shopping criterion only)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Unknown criteria or unsupported months window"
          }
        }
      }
    },
    "/v1/cellar/stock/{type}": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "List stock of a type (scope cellar/*:r)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expires_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated stock"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Add stock item (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fermentable",
                "hop",
                "yeast",
                "fining",
                "additive",
                "flavor"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "ingredient_id": {
                    "type": "integer"
                  },
                  "subtype": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "number",
                    "default": 0
                  },
                  "purchased_at": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/cellar/stock/{type}/{id}": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "Get stock item (scope cellar/*:r)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stock item"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "cellar"
        ],
        "summary": "Update stock item (type not changeable) (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "subtype": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "number"
                  },
                  "purchased_at": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "cellar"
        ],
        "summary": "Delete stock item (soft) (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/cellar/inventory": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "List packaged-beer inventory (scope cellar/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "batch_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InventoryItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Add inventory item (scope cellar/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "keg",
                      "bottle",
                      "can",
                      "other"
                    ],
                    "default": "bottle"
                  },
                  "batch_id": {
                    "type": "integer"
                  },
                  "volume": {
                    "type": "number",
                    "default": 0
                  },
                  "quantity": {
                    "type": "number",
                    "default": 0
                  },
                  "location": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/cellar/inventory/{id}": {
      "get": {
        "tags": [
          "cellar"
        ],
        "summary": "Get inventory item (scope cellar/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inventory item"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "cellar"
        ],
        "summary": "Update inventory item (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "volume": {
                    "type": "number"
                  },
                  "quantity": {
                    "type": "number"
                  },
                  "location": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "cellar"
        ],
        "summary": "Delete inventory item (soft) (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/share-design": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Prefilled design context for the 1080×1080 share picture — title, style, ingredient names, targets, photo path (owner only, scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{title, style, status, day, photo, grains[], hops[], yeast[], misc[], targets{og,fg,abv,ibu,ebc}, volume}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/brews/{sid}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Read-only shared view of a brew session — masthead facts, step timeline, per-stage additions when the linked recipe is public (scope social/*:r)",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{session: {id, name, owner_id, username, status, brew_date, batch_volume, og, fg, abv, style, label}, steps: [...], additions: {stage: [{name, type, kg, sec}]}}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/brews/{sid}/comments": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Brew-day chat on a shared brew session — comments live 24 h, like shouts (scope social/*:r)",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{session: {id, name, owner_id}, comments: [{id, user_id, username, body, image, created_at}]}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Comment on a brew day — JSON {body}, or multipart with an \"image\" file (brewer only) (scope social/*:rw)",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string"
                  },
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Brewer only — silently dropped for anyone else."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Empty comment / invalid image (validation_error | invalid_image)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/brew-comments/{id}": {
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Delete a brew-day comment — writer, brewer or admin (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/feed": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Wall timeline: shouts (24h lifetime, with images[] and expires_at) and activity from you + people you follow (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "batch",
                "recipe",
                "tasting"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/tap-list": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Your active tap list (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TapEntry"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Put one of your batches on tap (scope social/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "batch_id"
                ],
                "properties": {
                  "batch_id": {
                    "type": "integer"
                  },
                  "tapped_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/tap-list/{user_id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Another user's active tap list (scope social/*:r)",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated tap entries"
          }
        }
      }
    },
    "/v1/social/tap-list/{id}": {
      "put": {
        "tags": [
          "social"
        ],
        "summary": "Update a tap entry (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "is_active": {
                    "type": "boolean"
                  },
                  "tapped_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Remove tap entry (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/tasting-notes": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Your tasting notes (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "batch_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TastingNote"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Create tasting note — any existing batch, not just your own (scope social/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "batch_id"
                ],
                "properties": {
                  "batch_id": {
                    "type": "integer"
                  },
                  "rating": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 5
                  },
                  "appearance": {
                    "type": "string"
                  },
                  "aroma": {
                    "type": "string"
                  },
                  "taste": {
                    "type": "string"
                  },
                  "mouthfeel": {
                    "type": "string"
                  },
                  "overall": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/tasting-notes/{id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Get tasting note (own) (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Note"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "social"
        ],
        "summary": "Update tasting note (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "appearance": {
                    "type": "string"
                  },
                  "aroma": {
                    "type": "string"
                  },
                  "taste": {
                    "type": "string"
                  },
                  "mouthfeel": {
                    "type": "string"
                  },
                  "overall": {
                    "type": "string"
                  },
                  "rating": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Delete tasting note (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/follows": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Users you follow (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FollowEntry"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Follow a user (scope social/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already following"
          },
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/follows/{id}": {
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Unfollow — accepts relationship id or the followed user's id (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/followers": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Users following you (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated follow entries"
          }
        }
      }
    },
    "/v1/social/places": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Visible places: public+active, or ones you are a member of (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/places/{place_id}/channels": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Chat channels of a place (scope social/*:r)",
        "parameters": [
          {
            "name": "place_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Channels (not paginated)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatChannel"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/channels/{id}/messages": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Channel messages, chronological within page (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Send message — members only (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/channels/{id}/messages/since/{msg_id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Poll new messages after msg_id (max 200, ascending) (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "msg_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New messages (no meta)"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/dm/threads": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Your DM threads with unread counts (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DmThread"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/dm/start/{user_id}": {
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Start (or fetch existing) DM thread with a user (scope social/*:rw)",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Existing thread {id}"
          },
          "201": {
            "description": "New thread {id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/dm/threads/{id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Thread + last 200 messages; marks incoming as read (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{thread, messages[]}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "thread": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "messages": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DmMessage"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/dm/threads/{id}/messages": {
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Send DM (notifies recipient) (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/dm/threads/{id}/messages/since/{msg_id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Poll new DMs after msg_id (max 200) (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "msg_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New messages"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/events": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Public events (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "upcoming",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                0,
                1
              ],
              "default": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SocialEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/events/{id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Get event (private events only visible to owner) (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/reviews": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "List reviews (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "mine",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                1
              ]
            }
          },
          {
            "name": "subject_type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "lib_ingredient",
                "place",
                "place_product"
              ]
            }
          },
          {
            "name": "subject_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Review"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Create review — one per subject per user (scope social/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject_type",
                  "subject_id",
                  "rating"
                ],
                "properties": {
                  "subject_type": {
                    "type": "string",
                    "enum": [
                      "lib_ingredient",
                      "place",
                      "place_product"
                    ]
                  },
                  "subject_id": {
                    "type": "integer"
                  },
                  "rating": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "recommend": {
                    "type": "string",
                    "enum": [
                      "yes",
                      "no",
                      "neutral"
                    ],
                    "default": "neutral"
                  },
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "session_id": {
                    "type": "integer"
                  },
                  "batch_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Already reviewed (code: conflict)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/reviews/{id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Get review (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Review"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "social"
        ],
        "summary": "Update own review (subject not changeable) (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rating": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "recommend": {
                    "type": "string",
                    "enum": [
                      "yes",
                      "no",
                      "neutral"
                    ]
                  },
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Delete own review (soft) (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/measurements": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "List measurements (scope lab/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "session_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "batch_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "logger_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "measured_at >="
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "measured_at <="
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Measurement"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "lab"
        ],
        "summary": "Record a measurement (scope lab/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "value",
                  "unit"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "gravity",
                      "ph",
                      "temperature",
                      "pressure",
                      "brix",
                      "volume",
                      "other"
                    ],
                    "description": "Measurement type. Synonyms temp/sg/specific_gravity are accepted and mapped; anything else is a 422."
                  },
                  "value": {
                    "type": "number"
                  },
                  "unit": {
                    "type": "string"
                  },
                  "logger_id": {
                    "type": "integer"
                  },
                  "session_id": {
                    "type": "integer"
                  },
                  "batch_id": {
                    "type": "integer"
                  },
                  "instrument_id": {
                    "type": "integer"
                  },
                  "qty": {
                    "type": "number"
                  },
                  "measured_at": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — or folded into the latest matching row, in which case data.id is that row’s id and data.absorbed is true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "absorbed": {
                          "type": "boolean",
                          "description": "Present and true when the reading extended an existing run instead of creating a row."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "description": "Measurement limit reached: 288 per logger per 24 hours (measurements without a logger_id share one per-account bucket). Meant to stop a device stuck in a logging loop — check the device's posting interval."
          }
        },
        "description": "A logger-bound reading equal to the latest stored row’s value (within the type’s own step: gravity 3 decimals, temperature 1, ph 2) extends that row instead of inserting a duplicate — measured_at moves to last-seen, sample_count grows, span_started_at marks where the run began, and the response carries the existing row’s id with \"absorbed\": true. Runs break on a value change, on 45 minutes of silence, after one hour of span, and on backdated timestamps; manual readings (no logger_id), readings with notes and edited rows are never folded."
      }
    },
    "/v1/lab/measurements/{id}": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "Get measurement (scope lab/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Measurement"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "lab"
        ],
        "summary": "Delete measurement (soft) (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/loggers": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "List your loggers (scope lab/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Logger"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/lab/loggers/{logger_id}/measurements": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "Latest readings for a logger, newest first (scope lab/*:r)",
        "parameters": [
          {
            "name": "logger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Readings (no meta)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/equipment": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "List equipment profiles (scope lab/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated equipment profiles"
          }
        }
      },
      "post": {
        "tags": [
          "lab"
        ],
        "summary": "Create equipment profile (scope lab/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/lab/equipment/{id}": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "Get equipment profile (scope lab/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/EquipmentProfile"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "lab"
        ],
        "summary": "Update equipment profile (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "lab"
        ],
        "summary": "Delete equipment profile (soft) (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/instruments/{id}": {
      "post": {
        "tags": [
          "lab"
        ],
        "summary": "Instrument telemetry ingest. Body is channel-keyed values ({\"temperature\":18.5}) or {\"value\":n} shorthand. Channels/corrections/alarms come from the instrument's stored schema. Header-less devices can use POST /v1/ingest/{api_key}/lab/instruments/{id}. (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{instrument_id, fields_logged[], alarms[]}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "One instrument with channels, corrections and alarms (scope lab/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instrument"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/hub/api-keys": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "List API keys — full key never shown (scope hub/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "hub"
        ],
        "summary": "Create API key — the key is returned ONCE (scope hub/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": [
                      "*"
                    ]
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id, name, key (once), key_prefix, scopes, expires_at, created_at}"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/hub/api-keys/{id}": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "Get API key metadata (scope hub/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key metadata"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "hub"
        ],
        "summary": "Revoke API key (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/hub/instruments": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "List connected instruments (scope hub/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Instrument"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "hub"
        ],
        "summary": "Register instrument (scope hub/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "default": "other"
                  },
                  "connection_type": {
                    "type": "string",
                    "default": "connected"
                  },
                  "make": {
                    "type": "string"
                  },
                  "manufacturer": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id, name, type}"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/hub/instruments/{id}": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "Get instrument incl. data_schema/config (scope hub/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instrument row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "hub"
        ],
        "summary": "Telemetry ingest (legacy alias; dotted paths supported) (scope hub/instruments:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{instrument_id, log_id, primary_value, primary_unit, fields_logged[], alarms[]}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "put": {
        "tags": [
          "hub"
        ],
        "summary": "Update instrument metadata (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "make": {
                    "type": "string"
                  },
                  "manufacturer": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "hub"
        ],
        "summary": "Delete instrument (soft) (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/hub/instruments/{id}/detect-fields": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "Flatten last telemetry payload into typed field paths (scope hub/instruments:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[{path, type, value}]"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "No telemetry yet (code: no_data) or invalid payload (code: invalid_data)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/hub/webhooks": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "List webhooks — signing secret never exposed (scope hub/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "hub"
        ],
        "summary": "Create webhook. direction=out requires public http(s) url; returns signing secret ONCE. direction=in returns incoming_token for POST /v1/webhooks/in/{token}. (scope hub/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "direction": {
                    "type": "string",
                    "enum": [
                      "in",
                      "out"
                    ],
                    "default": "out"
                  },
                  "url": {
                    "type": "string",
                    "description": "Required for out"
                  },
                  "template": {
                    "type": "string"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true
                  },
                  "event_filter": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Globs over: batch.timer, batch.status, instrument.alarm, dm.received, webhook.test, webhook.incoming"
                  },
                  "severity_filter": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "info",
                        "warning",
                        "critical"
                      ]
                    }
                  },
                  "incoming_action": {
                    "type": "string",
                    "enum": [
                      "notification"
                    ]
                  },
                  "incoming_config": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id, direction, secret? (out, once), incoming_token? (in)}"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/hub/webhooks/{id}": {
      "get": {
        "tags": [
          "hub"
        ],
        "summary": "Get webhook (scope hub/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "hub"
        ],
        "summary": "Update webhook (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "hub"
        ],
        "summary": "Delete webhook (soft) (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/notifications": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "List notifications (scope hub/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "unread",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                1
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Notification"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/notifications/unread/count": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "Unread count (scope hub/*:r)",
        "responses": {
          "200": {
            "description": "{count}"
          }
        }
      }
    },
    "/v1/notifications/read-all": {
      "post": {
        "tags": [
          "notifications"
        ],
        "summary": "Mark all read (scope hub/*:rw)",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          }
        }
      }
    },
    "/v1/notifications/{id}/read": {
      "post": {
        "tags": [
          "notifications"
        ],
        "summary": "Mark one read (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/notifications/{id}": {
      "delete": {
        "tags": [
          "notifications"
        ],
        "summary": "Delete notification (scope hub/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/water-profiles": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "List water profiles — global presets + own (scope lab/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "purpose",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "source",
                "target"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WaterProfile"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "lab"
        ],
        "summary": "Create water profile (scope lab/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "source",
                      "target"
                    ],
                    "default": "source"
                  },
                  "ca": {
                    "type": "number"
                  },
                  "mg": {
                    "type": "number"
                  },
                  "na": {
                    "type": "number"
                  },
                  "so4": {
                    "type": "number"
                  },
                  "cl": {
                    "type": "number"
                  },
                  "hco3": {
                    "type": "number"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/lab/water-profiles/{id}": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "Get water profile (scope lab/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/WaterProfile"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "lab"
        ],
        "summary": "Update own water profile (globals are read-only) (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "lab"
        ],
        "summary": "Delete own water profile (soft) (scope lab/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/shops": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "List verified shop places (scope social/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Matches supplier or product name/description"
          },
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO code, e.g. NO"
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Supplier"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/social/shops/{id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Get shop place incl. active products (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Supplier",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Supplier"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/shops/{id}/products": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Shop place's active products (scope social/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupplierProduct"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/profiles/{user_id}": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "A user's Social profile with per-field visibility applied (scope social/*:r)",
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/SocialProfile"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/label": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Upload session label image (scope brewhouse/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "format": "binary",
                    "description": "JPEG/PNG/WebP; stored as JPEG. Field name 'label' or 'file'."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image_base64"
                ],
                "properties": {
                  "image_base64": {
                    "type": "string",
                    "description": "Base64-encoded JPEG/PNG/WebP (raw base64 or data: URI), max 4 MB decoded. Stored as JPEG, downscaled to ≤1024 px."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, label_path} / {avatar}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Missing/invalid image (validation_error | invalid_image)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Remove session label (scope brewhouse/*:rw)",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/v1/cellar/batches/{id}/label": {
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Upload batch label image (scope cellar/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "format": "binary",
                    "description": "JPEG/PNG/WebP; stored as JPEG. Field name 'label' or 'file'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, label_path} / {avatar}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Missing/invalid image (validation_error | invalid_image)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "cellar"
        ],
        "summary": "Remove batch label (scope cellar/*:rw)",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/v1/library/recipes/{id}/label": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Upload recipe label image (scope library/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "format": "binary",
                    "description": "JPEG/PNG/WebP; stored as JPEG. Field name 'label' or 'file'."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image_base64"
                ],
                "properties": {
                  "image_base64": {
                    "type": "string",
                    "description": "Base64-encoded JPEG/PNG/WebP (raw base64 or data: URI), max 4 MB decoded. Stored as JPEG, downscaled to ≤1024 px."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, label_path} / {avatar}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Missing/invalid image (validation_error | invalid_image)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "library"
        ],
        "summary": "Remove recipe label (scope library/*:rw)",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/v1/users/me/avatar": {
      "post": {
        "tags": [
          "users"
        ],
        "summary": "Upload your avatar — max 2 MB, resized to 256px (scope *:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "avatar"
                ],
                "properties": {
                  "avatar": {
                    "type": "string",
                    "format": "binary",
                    "description": "JPEG/PNG/WebP; stored as JPEG. Field name 'avatar' or 'file'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, label_path} / {avatar}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Missing/invalid image (validation_error | invalid_image)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "users"
        ],
        "summary": "Remove your avatar (scope *:rw)",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/users/me/notification-prefs": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Muted notification categories — enforced server-side across web, app, push and webhooks (scope *:r)",
        "responses": {
          "200": {
            "description": "{categories: [slug, …], muted: [slug, …]}"
          }
        }
      },
      "put": {
        "tags": [
          "users"
        ],
        "summary": "Replace the muted-category list (scope *:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "muted"
                ],
                "properties": {
                  "muted": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Category slugs to mute; unknown slugs are dropped. Empty array unmutes everything."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{muted: [slug, …]}"
          },
          "422": {
            "description": "'muted' missing or not an array (validation_error)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/library/articles": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List articles — public + own (scope library/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Article"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/library/articles/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get article incl. body (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Article",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Article"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/glossary": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List glossary terms — public + own (scope library/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GlossaryTerm"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/library/glossary/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get glossary term (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Term",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/GlossaryTerm"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/techniques": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "List techniques — public + own (scope library/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "difficulty",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Technique"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/library/techniques/{id}": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Get technique incl. body (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "no",
                "de",
                "es",
                "sv",
                "da",
                "cs",
                "pl",
                "uk"
              ]
            },
            "description": "Apply community translations (public entries); falls back to original text"
          }
        ],
        "responses": {
          "200": {
            "description": "Technique",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Technique"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/events/{id}/rsvp": {
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Set or clear your RSVP; returns updated counts (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "going",
                      "interested",
                      "not_going",
                      "clear"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{event_id, my_status, going, interested, not_going}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/cellar/batches/{id}/share": {
      "post": {
        "tags": [
          "cellar"
        ],
        "summary": "Enable the public batch page (QR on the bottle). Idempotent; returns share_url. (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, share_token, share_url}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "cellar"
        ],
        "summary": "Revoke the public batch page — link and QR go dead (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/ingest/{api_key}/lab/instruments/{id}": {
      "post": {
        "operationId": "ingestInstrumentTelemetry",
        "summary": "Instrument telemetry via path-embedded key",
        "description": "Alias for POST /v1/lab/instruments/{id} with the API key embedded in the path, for constrained devices (iSpindel, Tilt bridges, RAPT webhooks) that cannot set an Authorization header. Any POST route can be reached the same way: POST /v1/ingest/{api_key}/<path-under-v1>. This alias also answers on plain HTTP (port 80) for old firmware without TLS support — prefer HTTPS whenever possible. Requires a key with hub/instruments:rw and lab/*:rw scopes (the \"Sensor\" preset).",
        "tags": [
          "ingest"
        ],
        "security": [],
        "parameters": [
          {
            "name": "api_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The API key (acts as the credential — treat the URL as a secret)"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Instrument id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "description": "Channel-keyed readings — any numeric fields; unmapped fields show up in field detection"
              },
              "example": {
                "gravity": 1.048,
                "temp": 20.3,
                "battery": 87
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reading accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired API key"
          },
          "403": {
            "description": "Key lacks the required scopes"
          }
        }
      }
    },
    "/v1/push/vapid-key": {
      "get": {
        "operationId": "pushVapidKey",
        "summary": "VAPID application server key",
        "description": "Public key for PushManager.subscribe() in a PWA. Requires any valid API key.",
        "tags": [
          "push"
        ],
        "responses": {
          "200": {
            "description": "Key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/push/subscriptions": {
      "post": {
        "operationId": "pushSubscribe",
        "summary": "Register a Web Push subscription",
        "description": "Registers this device for the key owner's notifications. Upserts by endpoint.",
        "tags": [
          "push"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "endpoint",
                  "keys"
                ],
                "properties": {
                  "endpoint": {
                    "type": "string"
                  },
                  "keys": {
                    "type": "object",
                    "required": [
                      "p256dh",
                      "auth"
                    ],
                    "properties": {
                      "p256dh": {
                        "type": "string"
                      },
                      "auth": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscribed"
          },
          "422": {
            "description": "Validation error"
          }
        }
      }
    },
    "/v1/push/unsubscribe": {
      "post": {
        "operationId": "pushUnsubscribe",
        "summary": "Remove a Web Push subscription",
        "tags": [
          "push"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "endpoint"
                ],
                "properties": {
                  "endpoint": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unsubscribed"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/next": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Next uncompleted step with state, position and its additions — one read-aloud-ready answer (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Next step, or all_steps_done"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/adjustments": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Record a deviation from the plan (kind: skip|adjust|add). The plan is never edited — deviations live beside the frozen snapshot and targets_adjusted is re-estimated. skip/adjust echo the _ref+_fp pair from sessionGet's plan_additions (verified server-side; 409 on mismatch). Units: amount in grams (kg for fermentables; litres for a planned addition whose plan_additions row says unit \"l\"), time in minutes (days in fermentation). One adjust per ref — a new one replaces it. Allowed while brewing or completed. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "stage"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "skip",
                      "adjust",
                      "add"
                    ]
                  },
                  "stage": {
                    "type": "string",
                    "enum": [
                      "prep",
                      "mash",
                      "sparge",
                      "boil",
                      "whirlpool",
                      "fermentation",
                      "carbonation",
                      "other"
                    ]
                  },
                  "ref": {
                    "type": "string",
                    "description": "skip/adjust: _ref from plan_additions"
                  },
                  "fp": {
                    "type": "string",
                    "description": "skip/adjust: _fp from plan_additions"
                  },
                  "name": {
                    "type": "string",
                    "description": "add: ingredient name"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "hop",
                      "fermentable",
                      "additive",
                      "flavor"
                    ]
                  },
                  "amount": {
                    "type": "number",
                    "description": "grams; kg for fermentables"
                  },
                  "time": {
                    "type": "number",
                    "description": "minutes; days in fermentation"
                  },
                  "alpha": {
                    "type": "number",
                    "description": "add hops: alpha acid %"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{adjustments: [...], targets_adjusted: {...}|null}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "invalid_state — adjustments can only be recorded on brewing or completed sessions; or ref_mismatch — the referenced addition no longer matches the plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/adjustments/{adj_id}": {
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Undo one recorded deviation; targets_adjusted is re-estimated. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "adj_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{adjustments: [...], targets_adjusted: {...}|null}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/editor": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Editor-format view of a planning session's plan (same shape as the recipe editor endpoints). (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/save-as-recipe": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Create a Library recipe from the session's snapshot — the plan, or as brewed with adjustments (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variant": {
                    "type": "string",
                    "enum": [
                      "plan",
                      "adjusted"
                    ],
                    "default": "plan",
                    "description": "'plan' copies the frozen snapshot as-is; 'adjusted' folds the recorded deviations into it first."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "New recipe id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/plan": {
      "put": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Replace a planning session's plan from editor-format data — snapshot and steps are rebuilt together by the shared writer. Planning sessions only. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Editor-format document (same shape sessionEditorGet returns)."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{session_id, targets}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/stock-check": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Cross-check the session's planned additions against cellar stock: per-ingredient status (ok/short/missing) and shopping-list flag. (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/stock-toggle": {
      "post": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Toggle the shopping-list state for one planned ingredient ({type, name}); returns the fresh assessment row. (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "name"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "shoppable ingredient type"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "assessment row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/recipes/{id}/stock-check": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Cross-check the recipe's ingredients against cellar stock: per-ingredient status (ok/short/missing) and shopping-list flag. (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/library/recipes/{id}/stock-toggle": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Toggle the shopping-list state for one recipe ingredient ({type, name}); returns the fresh assessment row. (scope cellar/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "name"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "shoppable ingredient type"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "assessment row"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/recipes/editor": {
      "post": {
        "tags": [
          "library"
        ],
        "summary": "Create a recipe from editor-format data (the web/PWA editor's shape; converted through the one shared mapping). (scope library/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Editor-format document."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id}"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/library/recipes/{id}/editor": {
      "get": {
        "tags": [
          "library"
        ],
        "summary": "Editor-format view of a recipe. (scope library/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "library"
        ],
        "summary": "Replace a recipe from editor-format data. (scope library/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Editor-format document."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/loggers": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Logger readings for a session — live rows while brewing, the frozen snapshot after completion (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{loggers: [{name, type, readings[]}]}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/social/ignores": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Brewers the caller ignores (soft one-way mute) (scope social/*:r)",
        "responses": {
          "200": {
            "description": "{items: [{id, user_id, username, created_at}]}"
          }
        }
      },
      "post": {
        "tags": [
          "social"
        ],
        "summary": "Ignore a brewer — hides their content from the caller's Social; they are not notified (scope social/*:rw)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{id}"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    },
    "/v1/social/ignores/{id}": {
      "delete": {
        "tags": [
          "social"
        ],
        "summary": "Stop ignoring — the ignore row id from GET /v1/social/ignores (scope social/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/lab/instruments": {
      "get": {
        "tags": [
          "lab"
        ],
        "summary": "The caller's instruments with channel schemas and derived status (scope lab/*:r)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated instruments"
          }
        }
      }
    },
    "/v1/social/events/countries": {
      "get": {
        "tags": [
          "social"
        ],
        "summary": "Countries that have events, with counts — feeds the country filter (scope social/*:r)",
        "responses": {
          "200": {
            "description": "{items: [{code, name, n}]}"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/stage-loggers": {
      "get": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Which logger is attached to which stage of the session — the binding the per-stage readings and a follow-mode instrument alarm resolve through. Same rows as sessionGet's stage_loggers. (scope brewhouse/*:r)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "list of {stage, logger_id, logger_name, logger_type, unit}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "stage": {
                        "type": "string",
                        "enum": [
                          "prep",
                          "mash",
                          "sparge",
                          "boil",
                          "whirlpool",
                          "chill",
                          "fermentation",
                          "carbonation",
                          "other"
                        ]
                      },
                      "logger_id": {
                        "type": "integer"
                      },
                      "logger_name": {
                        "type": "string"
                      },
                      "logger_type": {
                        "type": "string",
                        "enum": [
                          "gravity",
                          "ph",
                          "temperature",
                          "pressure",
                          "brix",
                          "volume",
                          "other"
                        ]
                      },
                      "unit": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/brewhouse/sessions/{id}/stage-loggers/{stage}": {
      "put": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Attach a logger to a stage — one logger per stage, a new one replaces the old. A temperature or pressure logger bound here is what a \"follow brew step\" instrument alarm tracks: limits = the running step's target_temp/target_pressure ± the configured deviation; the last-started step wins when several run. Logger ids come from GET /v1/lab/loggers. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "stage",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "prep",
                "mash",
                "sparge",
                "boil",
                "whirlpool",
                "chill",
                "fermentation",
                "carbonation",
                "other"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "logger_id"
                ],
                "properties": {
                  "logger_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{session_id, stage, logger_id, logger_name, logger_type, unit}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stage": {
                      "type": "string",
                      "enum": [
                        "prep",
                        "mash",
                        "sparge",
                        "boil",
                        "whirlpool",
                        "chill",
                        "fermentation",
                        "carbonation",
                        "other"
                      ]
                    },
                    "logger_id": {
                      "type": "integer"
                    },
                    "logger_name": {
                      "type": "string"
                    },
                    "logger_type": {
                      "type": "string",
                      "enum": [
                        "gravity",
                        "ph",
                        "temperature",
                        "pressure",
                        "brix",
                        "volume",
                        "other"
                      ]
                    },
                    "unit": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      },
      "delete": {
        "tags": [
          "brewhouse"
        ],
        "summary": "Detach the stage's logger. 204 whether or not one was attached. (scope brewhouse/*:rw)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "stage",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "prep",
                "mash",
                "sparge",
                "boil",
                "whirlpool",
                "chill",
                "fermentation",
                "carbonation",
                "other"
              ]
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Developer docs — authentication, webhooks, instruments and the MCP server",
    "url": "https://web.dynbrew.com/hub/docs"
  }
}
