{"openapi":"3.0.3","info":{"title":"Karst Firewall 5.0 API","version":"2026-07-03","description":"Public OpenAPI contract for the kf50-php API application, including authentication, IoT ingestion, PyroWISE/vegetation-health access, technology logs, the field positioning mobile app, and the DJI drone bridge write-back endpoints.","contact":{"name":"Infordata Sistemi","url":"https://www.infordata.it/"}},"servers":[{"url":"http://kf50.infordata.it/api","description":"KF50 API base URL"}],"tags":[{"name":"Authentication","description":"User token lifecycle for the API app."},{"name":"Technology logs","description":"CRUD and ChirpStack-style ingestion for deployed technology logs."},{"name":"Wildfire data","description":"Historical wildfire and calibration export surfaces."},{"name":"Vegetation health","description":"NDVI vegetation-health reports for headless agents."},{"name":"IoT inventory","description":"Read-only sensor and gateway inventory for ingest services."},{"name":"IoT write-back","description":"Sensor/gateway discovery and metrics write-back."},{"name":"Field positioning","description":"Placement campaigns and as-built confirmation for the field positioning mobile app."},{"name":"IoT field ops","description":"Installer-app device lookup, bulk import, and post-install status checks."},{"name":"Field maintenance","description":"Field-asset maintenance events for the maintenance mobile app (per-user JWT)."},{"name":"DJI bridge","description":"kf50-dji-bridge write-back and hotspot trigger endpoints."}],"paths":{"/auth/login":{"post":{"operationId":"authLogin","tags":["Authentication"],"summary":"Login and receive auth data","description":"Authenticates a user with the API app credentials and returns the user auth payload, including the bearer token when successful.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthData"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}}}},"/auth/signup":{"post":{"operationId":"authSignup","tags":["Authentication"],"summary":"Create an account","description":"Creates a user account from the public signup payload. The response is true on success or validation details on failure.","responses":{"200":{"description":"Boolean success flag.","content":{"application/json":{"schema":{"type":"boolean"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}}}},"/auth/me":{"get":{"operationId":"authMe","tags":["Authentication"],"summary":"Read current user auth data","description":"Returns the authenticated user auth payload.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthData"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}]}},"/auth/refresh":{"post":{"operationId":"authRefresh","tags":["Authentication"],"summary":"Rotate the session token","description":"Accepts a bearer token in the Authorization header or authToken in the request body and returns fresh auth data. Rotation: the response carries a NEW authToken and the presented token only stays valid for a short overlap window, so clients must store the returned token.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthData"}}}},"401":{"description":"Token missing or invalid."},"422":{"description":"Validation failed."}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}}}}},"/auth/forgot-password":{"post":{"operationId":"authForgotPassword","tags":["Authentication"],"summary":"Request password reset email","description":"Starts the password reset workflow for a user email address.","responses":{"200":{"description":"Boolean success flag.","content":{"application/json":{"schema":{"type":"boolean"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}}}}},"/auth/reset-password":{"post":{"operationId":"authResetPassword","tags":["Authentication"],"summary":"Reset password","description":"Completes a password reset with the token from the email workflow.","responses":{"200":{"description":"Boolean success flag.","content":{"application/json":{"schema":{"type":"boolean"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"parameters":[{"name":"token","in":"query","required":true,"description":"Password reset token.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}}}}},"/karst/meteo-briefing/today":{"get":{"operationId":"karstMeteoBriefingToday","tags":["Karst operator dashboard"],"summary":"Daily meteo briefing card","description":"Returns the normalized daily meteo-briefing payload for the operator cockpit card (weather synthesis, FWI, MeteoAlarm highlights). Intentionally public: the same Karst risk picture is already on the public portal; responses are edge-cacheable for 5 minutes and may be served stale (X-KF50-Stale: 1) when an upstream is down.","responses":{"200":{"description":"Briefing payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenericObject"}}}},"503":{"description":"Briefing service unavailable (all upstreams down, no cached copy)."}},"parameters":[{"name":"lang","in":"query","required":false,"description":"Briefing language.","schema":{"type":"string","enum":["it","sl","en","de"],"default":"it"}},{"name":"aoi","in":"query","required":false,"description":"AOI profile id; defaults to the configured Karst profile.","schema":{"type":"string"}}]}},"/technology-log":{"get":{"operationId":"technologyLogIndex","tags":["Technology logs"],"summary":"List technology logs","description":"Lists active, non-deleted technology log rows through the TechnologyLogSearch data provider. Pass page=0 to disable pagination.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLogList"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"description":"Page number. Use 0 to disable pagination.","schema":{"oneOf":[{"type":"integer"},{"type":"string"}]}}]},"post":{"operationId":"technologyLogCreate","tags":["Technology logs"],"summary":"Create a technology log","description":"Creates a technology log row using the Yii REST ActiveController create action.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLog"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLog"}}}}},"security":[{"userBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLogCreate"}}}}}},"/technology-log/{id}":{"get":{"operationId":"technologyLogView","tags":["Technology logs"],"summary":"Read one technology log","description":"Reads a single technology log row by id.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLog"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Technology log id.","schema":{"type":"integer","format":"int64"}}]},"put":{"operationId":"technologyLogUpdate","tags":["Technology logs"],"summary":"Update one technology log","description":"Updates a technology log row. The REST controller accepts PUT for updates.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLog"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Technology log id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLogCreate"}}}}},"delete":{"operationId":"technologyLogDelete","tags":["Technology logs"],"summary":"Soft-delete one technology log","description":"Marks a technology log as deleted when the model has a deleted attribute; otherwise deletes the row.","responses":{"200":{"description":"Boolean success flag.","content":{"application/json":{"schema":{"type":"boolean"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Technology log id.","schema":{"type":"integer","format":"int64"}}]}},"/technology-log/validation":{"get":{"operationId":"technologyLogValidation","tags":["Technology logs"],"summary":"Read validation rules","description":"Returns Yii validation rules for the TechnologyLog model.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationRules"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}]}},"/technology-log/create-from-json":{"post":{"operationId":"technologyLogCreateFromJson","tags":["Technology logs"],"summary":"Create technology log from ChirpStack JSON","description":"Maps a ChirpStack-style payload by deviceInfo.devEui to a Technology row and stores object data as a technology log.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnologyLog"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChirpstackTechnologyLogRequest"}}}}}},"/v1/historical-wildfire":{"get":{"operationId":"v1HistoricalWildfire","tags":["Wildfire data"],"summary":"List historical wildfires as GeoJSON","description":"Returns a paginated GeoJSON FeatureCollection of historical wildfire rows, optionally filtered by bbox=south,west,north,east.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJsonFeatureCollection"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"bbox","in":"query","required":false,"description":"Optional south,west,north,east bbox.","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"Page number, starting at 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Rows per page, 1-200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}]}},"/v1/calibration/export":{"post":{"operationId":"v1CalibrationExport","tags":["Wildfire data"],"summary":"Create calibration export","description":"Creates a calibration-export bundle for eligible simulation runs and responds with a redirect to the download endpoint.","responses":{"302":{"description":"Calibration export created. Location header points to /v1/calibration/download/{id}."},"400":{"description":"run_ids missing."},"404":{"description":"One or more runs not found."},"422":{"description":"Run is not eligible for calibration export."}},"security":[{"userBearer":[]}],"parameters":[{"name":"run_ids","in":"query","required":false,"description":"Comma-separated run ids. May also be supplied in the JSON body.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalibrationExportRequest"}}}}}},"/v1/calibration/download/{id}":{"get":{"operationId":"v1CalibrationDownload","tags":["Wildfire data"],"summary":"Download calibration export","description":"Downloads a generated calibration export file by id.","responses":{"200":{"description":"Export file.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Export not found."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Calibration export id.","schema":{"type":"integer","format":"int64"}}]}},"/v1/veg-health/ingest-status":{"get":{"operationId":"vegHealthIngestStatus","tags":["Vegetation health"],"summary":"Read NDVI ingest status","description":"Returns recent NDVI ingest runs, freshness and degradation metadata. Requires pyrowise.ndvi.view and the NDVI feature flag.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VegetationIngestStatus"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"action","in":"query","required":false,"description":"Optional action filter: poll-ndvi or poll-ndvi-baseline.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Rows to return, 1-100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}]}},"/v1/veg-health/aoi/{aoi_id}/series":{"get":{"operationId":"vegHealthSeries","tags":["Vegetation health"],"summary":"Read NDVI time series for one AOI","description":"Returns NDVI observations for an AOI across a date window.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VegetationSeries"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"aoi_id","in":"path","required":true,"description":"AOI profile id.","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"description":"Start date YYYY-MM-DD. Default is 180 days before to.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"End date YYYY-MM-DD. Default is today.","schema":{"type":"string","format":"date"}},{"name":"observed_only","in":"query","required":false,"description":"1 to drop cloud/gap-filled scenes.","schema":{"type":"integer","enum":[0,1],"default":0}},{"name":"limit","in":"query","required":false,"description":"Rows to return, 1-1000.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":500}}]}},"/v1/veg-health/aoi/{aoi_id}":{"get":{"operationId":"vegHealthAoi","tags":["Vegetation health"],"summary":"Read current vegetation-health verdict","description":"Returns the latest vegetation-health report for an AOI, including current band, anomaly and fuel-class breakdown when available.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VegetationHealthReport"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"aoi_id","in":"path","required":true,"description":"AOI profile id.","schema":{"type":"string"}}]}},"/iot-sensor-inventory.json":{"get":{"operationId":"iotSensorInventoryJson","tags":["IoT inventory"],"summary":"List active IoT sensors","description":"Read-only active sensor inventory served to kf50-observation-ingest.","responses":{"200":{"description":"Array of active sensors.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IotSensorInventoryItem"}}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}]}},"/iot-sensor-inventory":{"get":{"operationId":"iotSensorInventoryAlias","tags":["IoT inventory"],"summary":"List active IoT sensors","description":"Read-only active sensor inventory served to kf50-observation-ingest.","responses":{"200":{"description":"Array of active sensors.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IotSensorInventoryItem"}}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}]}},"/iot-gateway-inventory.json":{"get":{"operationId":"iotGatewayInventoryJson","tags":["IoT inventory"],"summary":"List active IoT gateways","description":"Read-only active gateway inventory served to kf50-observation-ingest.","responses":{"200":{"description":"Array of active gateways.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IotGatewayInventoryItem"}}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}]}},"/iot-gateway-inventory":{"get":{"operationId":"iotGatewayInventoryAlias","tags":["IoT inventory"],"summary":"List active IoT gateways","description":"Read-only active gateway inventory served to kf50-observation-ingest.","responses":{"200":{"description":"Array of active gateways.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IotGatewayInventoryItem"}}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}]}},"/placement-plans":{"get":{"operationId":"placementPlanIndex","tags":["Field positioning"],"summary":"List active placement campaigns","description":"Active placement campaigns, newest first, for the mobile app start screen. Returns headline fields plus per-status site counts, never the raw planner response.","responses":{"200":{"description":"Active campaigns.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementPlanList"}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}]}},"/placement-plan/{id}":{"get":{"operationId":"placementPlanView","tags":["Field positioning"],"summary":"Read one placement campaign and its sites","description":"One campaign plus every planned site — the LoRaMiP gateway mast and the e-nose sensors — each with its planned and (once confirmed) as-built coordinates. The optional assigned_to / status query filters narrow the sites to one crew member's queue; the plan summary still reflects the whole campaign.","responses":{"200":{"description":"Campaign and its sites.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementPlanDetail"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Placement plan not found."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Placement plan id.","schema":{"type":"integer","format":"int64"}},{"name":"assigned_to","in":"query","required":false,"description":"Filter the returned sites to this field operator (a \"my sites\" view).","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filter the returned sites to one lifecycle status (planned/approved/procured/installed/live/rejected/abandoned).","schema":{"type":"string"}}]}},"/placement-plan/site/{id}/confirm":{"post":{"operationId":"placementPlanConfirmSite","tags":["Field positioning"],"summary":"Confirm an as-built site position","description":"The mobile app writeback for an install visit: the device serial scanned off the hardware and the as-built GPS position from the phone. The planned position never changes; the confirmed position lands in installed_lat/lon so plan-vs-built drift stays auditable. Idempotent for a site already installed; sites that are live or in a terminal state refuse with 409. If the scanned serial is already held by another non-terminal site the call returns 409 code=serial_conflict with the holding site; re-POST with reassign=true to move the serial here (the old site is walked back to procured and its as-built facts are preserved in the audit log).","responses":{"200":{"description":"Site confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementConfirmResponse"}}}},"400":{"description":"Missing or invalid body — device_serial required (≤64 chars); lat/lon numeric and within WGS84 range."},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Placement site not found."},"409":{"description":"Site is live or in a terminal state; the deployment schema predates the positioning API; or the device serial is already held by another site (code=serial_conflict, includes holding_site — retry with reassign=true to move it)."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Placement site (candidate) id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementConfirmRequest"}}}}}},"/placement-plan/site/{id}/release":{"post":{"operationId":"placementPlanReleaseSite","tags":["Field positioning"],"summary":"Release a sensor from a site","description":"Removes the sensor from a placement site: clears the device serial and as-built position and walks the site back to procured (device available, no longer placed). Idempotent — releasing an already-empty site is a no-op (released=false). Allowed from any lifecycle state; the extra confirmation for a live (transmitting) sensor is enforced by the app. The prior serial/position/state are preserved in the audit log.","responses":{"200":{"description":"Site released, or already empty (released=false)."},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Placement site not found."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Placement site (candidate) id.","schema":{"type":"integer","format":"int64"}}]}},"/placement-plan/{id}/geojson":{"get":{"operationId":"placementPlanViewGeojson","tags":["Field positioning"],"summary":"Read one placement campaign as GeoJSON","description":"The same sites as /placement-plan/{id}, serialized as a GeoJSON FeatureCollection so the mobile app can drop them straight onto flutter_map/MapLibre. Each feature carries planned/as-built coordinates, status colour and server-side plan-vs-built drift. The optional assigned_to / status query filters narrow the features to one crew member's queue.","responses":{"200":{"description":"Campaign sites as a GeoJSON FeatureCollection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJsonFeatureCollection"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Placement plan not found."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Placement plan id.","schema":{"type":"integer","format":"int64"}},{"name":"assigned_to","in":"query","required":false,"description":"Filter the returned features to this field operator (a \"my sites\" view).","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filter the returned features to one lifecycle status (planned/approved/procured/installed/live/rejected/abandoned).","schema":{"type":"string"}}]}},"/placement-plan/site/{id}/photo":{"post":{"operationId":"placementPlanSitePhoto","tags":["Field positioning"],"summary":"Attach an install photo to a placement site","description":"Uploads one install photo (jpeg/png/webp ≤5 MB) for a placement site. Kept separate from /confirm so the offline app can retry the photo independently. Optional caption and as-built lat/lon may accompany the file. Supply client_uuid to make the retry safe: a replay returns the photo already stored (deduplicated=true) rather than a second copy.","responses":{"200":{"description":"Photo stored, or returned unchanged when this upload was already stored under the same client_uuid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementPhotoResponse"}}}},"400":{"description":"Missing or invalid file — file required; must be jpeg/png/webp ≤5 MB. Also returned for a malformed client_uuid."},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Placement site not found."},"409":{"description":"Deployment schema predates the placement-photo table, or client_uuid already identifies a photo on another site."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Placement site (candidate) id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Install photo (jpeg/png/webp ≤5 MB)."},"caption":{"type":"string","description":"Optional photo caption."},"lat":{"type":"number","description":"Optional as-built latitude (WGS84) where the photo was taken."},"lon":{"type":"number","description":"Optional as-built longitude (WGS84) where the photo was taken."},"client_uuid":{"type":"string","format":"uuid","description":"Client-generated UUID identifying this queued write. Replaying it returns the row already created instead of duplicating it. Optional."}},"required":["file"]}}}}}},"/iot-sensor/by-serial/{serial}":{"get":{"operationId":"iotSensorBySerial","tags":["IoT field ops"],"summary":"Look up one IoT sensor by serial","description":"Resolves one device by device_id or sensor_id_alias (the scanned QR may carry either) and returns its inventory record plus live freshness so the installer sees everything on scan. Responds 200 with {\"found\": false} when the serial is unknown.","responses":{"200":{"description":"Device lookup result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLookup"}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"serial","in":"path","required":true,"description":"Device serial: device_id or sensor_id_alias from the QR.","schema":{"type":"string"}}]}},"/iot-sensor-inventory/import":{"post":{"operationId":"iotSensorInventoryImport","tags":["IoT field ops"],"summary":"Bulk import IoT sensors by serial","description":"Pre-loads the whole device batch (serials, optional placements, gateway) so scans resolve in the field. Upserts by device_id with a per-row report. approve:true sets discovery_state=approved so imported devices are immediately scan-resolvable.","responses":{"200":{"description":"Per-row import report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorImportResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorImportRequest"}}}}}},"/iot-sensor/{device_id}/status":{"get":{"operationId":"iotSensorStatus","tags":["IoT field ops"],"summary":"Read one sensor status and metrics","description":"The post-install \"test connection\" read: the denormalized last_* snapshot, freshness, gateway link, per-metric ok/warn/alert levels, and the last N sensor-log rows when present. The app polls this until freshness=live.","responses":{"200":{"description":"Sensor status and metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorStatusView"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Sensor not found."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"description":"Sensor device id.","schema":{"type":"string"}}]}},"/iot-discovery/sensor":{"post":{"operationId":"iotDiscoverySensor","tags":["IoT write-back"],"summary":"Register discovered IoT sensor","description":"Called by kf50-observation-ingest when it sees a paired sensor device_id that has not been announced yet.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorDiscoveryResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"201":{"description":"Sensor row created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorDiscoveryResponse"}}}}},"security":[{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorDiscoveryRequest"}}}}}},"/iot-gateway/{serial}/status":{"get":{"operationId":"iotGatewayStatusRead","tags":["IoT field ops"],"summary":"Read gateway connectivity status","description":"Exposes the already-ingested gateway health for the installer app's \"validate gateway\" step: network/mqtt/influx status, last-seen, and paired/pending-sync counts.","responses":{"200":{"description":"Gateway connectivity status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotGatewayStatusView"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Gateway not found."}},"security":[{"userBearer":[]},{"internalBearer":[]}],"parameters":[{"name":"serial","in":"path","required":true,"description":"Gateway serial, 1-16 hex chars.","schema":{"type":"string"}}]},"post":{"operationId":"iotGatewayStatus","tags":["IoT write-back"],"summary":"Update gateway status","description":"Updates the last-known status projection for an already registered LoRaMIP gateway.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotGatewayStatusResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"internalBearer":[]}],"parameters":[{"name":"serial","in":"path","required":true,"description":"Gateway serial, 1-16 hex chars.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotGatewayStatusRequest"}}}}}},"/iot-sensor/{device_id}/last-metrics":{"post":{"operationId":"iotSensorLastMetrics","tags":["IoT write-back"],"summary":"Update one sensor last metrics projection","description":"Updates denormalised last_* telemetry fields for one registered sensor.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLastMetricsResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"internalBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"description":"Sensor device id, 1-16 hex chars.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLastMetricsRequest"}}}}}},"/iot-sensor/last-metrics":{"post":{"operationId":"iotSensorLastMetricsBulk","tags":["IoT write-back"],"summary":"Bulk update sensor last metrics projections","description":"Updates denormalised last_* telemetry fields for multiple registered sensors and returns a per-row report.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLastMetricsBulkResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLastMetricsBulkRequest"}}}}}},"/iot-sensor-log":{"post":{"operationId":"iotSensorLogFallback","tags":["IoT write-back"],"summary":"Persist sensor-log rows","description":"Idempotent per-reading sink used by kf50-observation-ingest for live MQTT history and explicit InfluxDB backfills.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLogFallbackResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"503":{"description":"Fallback disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisabledResponse"}}}}},"security":[{"internalBearer":[]}],"parameters":[{"name":"X-Influx-Fallback-Reason","in":"header","required":false,"description":"Optional degraded-mode reason stored on rows.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IotSensorLogFallbackRequest"}}}}}},"/iot-drone/register-discovery":{"post":{"operationId":"djiRegisterDiscovery","tags":["DJI bridge"],"summary":"Register discovered DJI drone","description":"Called by kf50-dji-bridge when a DJI airframe serial is first seen. Idempotent: existing serials return the existing row.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiDroneDiscoveryResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"201":{"description":"Drone row created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiDroneDiscoveryResponse"}}}}},"security":[{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiDroneDiscoveryRequest"}}}}}},"/iot-drone/{dji_serial}/status":{"post":{"operationId":"djiDroneStatus","tags":["DJI bridge"],"summary":"Update DJI drone status","description":"Updates last battery, network, dock, mission and observation timestamp for a registered DJI airframe.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiDroneStatusResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"internalBearer":[]}],"parameters":[{"name":"dji_serial","in":"path","required":true,"description":"DJI airframe serial.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiDroneStatusRequest"}}}}}},"/iot-mission/start":{"post":{"operationId":"djiMissionStart","tags":["DJI bridge"],"summary":"Start DJI mission","description":"Creates an in-flight mission row linked to a registered DJI drone. mission_id is bridge-owned and must be unique.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiMissionStartResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"201":{"description":"Mission row created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiMissionStartResponse"}}}}},"security":[{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiMissionStartRequest"}}}}}},"/iot-mission/{mission_id}/end":{"post":{"operationId":"djiMissionEnd","tags":["DJI bridge"],"summary":"End DJI mission","description":"Marks a mission ended and stores generated drone artefact URLs/counts.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiMissionEndResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"internalBearer":[]}],"parameters":[{"name":"mission_id","in":"path","required":true,"description":"DJI bridge mission id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiMissionEndRequest"}}}}}},"/pyrowise/trigger-from-hotspot":{"post":{"operationId":"djiTriggerFromHotspot","tags":["DJI bridge"],"summary":"Trigger PyroWISE nowcast from DJI hotspot","description":"Validates a drone-detected thermal hotspot, gates by confidence, calls PyroWISE nowcast, persists a simulation_run row, and links it to the DJI mission.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiHotspotTriggerResponse"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Below confidence threshold or invalid mission reference."},"201":{"description":"PyroWISE run accepted and persisted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiHotspotTriggerResponse"}}}},"400":{"description":"Malformed hotspot payload."},"502":{"description":"PyroWISE unreachable or returned an error."}},"security":[{"internalBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DjiHotspotTriggerRequest"}}}}}},"/field-assets":{"get":{"operationId":"fieldAssetIndex","tags":["Field maintenance"],"summary":"List field assets due, overdue, or nearby","description":"The maintenance app's work list: field-asset summaries with per-asset next_due_at, open-event count and overdue flag. Geo filter (near + radius_km) is a bounding-box prefilter plus haversine sort and returns distance_km; due/state filters narrow the queue.","responses":{"200":{"description":"Matching field assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldAssetList"}}}},"401":{"description":"Invalid or missing bearer token."}},"security":[{"userBearer":[]}],"parameters":[{"name":"near","in":"query","required":false,"description":"Optional \"lat,lon\" origin; sorts by distance and returns distance_km.","schema":{"type":"string"}},{"name":"radius_km","in":"query","required":false,"description":"Optional radius (km) around near.","schema":{"type":"number"}},{"name":"due_within_days","in":"query","required":false,"description":"Only assets whose next_due_at falls within this many days.","schema":{"type":"integer"}},{"name":"overdue_only","in":"query","required":false,"description":"1 to return only overdue assets.","schema":{"type":"integer","enum":[0,1],"default":0}},{"name":"open_only","in":"query","required":false,"description":"1 to return only assets with an open maintenance event.","schema":{"type":"integer","enum":[0,1],"default":0}},{"name":"taxonomy","in":"query","required":false,"description":"Filter by field-asset taxonomy code.","schema":{"type":"string"}},{"name":"deployment_state","in":"query","required":false,"description":"Filter by deployment state.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Free-text search over alias/serial.","schema":{"type":"string"}}]}},"/field-asset/by-serial/{serial}":{"get":{"operationId":"fieldAssetBySerial","tags":["Field maintenance"],"summary":"Look up one field asset by serial","description":"Scan lookup: matches field_asset.serial or alias, and falls through iot_sensor.device_id → field_asset_id, so a device scanned in either mobile app resolves to the same physical asset. Returns the asset with its events, photos and taxonomy checklist template.","responses":{"200":{"description":"Field-asset detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldAssetDetail"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Field asset not found."}},"security":[{"userBearer":[]}],"parameters":[{"name":"serial","in":"path","required":true,"description":"Asset serial, alias, or device id from the QR.","schema":{"type":"string"}}]}},"/field-asset/{id}":{"get":{"operationId":"fieldAssetView","tags":["Field maintenance"],"summary":"Read one field asset and its history","description":"One field asset plus its recent maintenance events, photos and the taxonomy checklist template used to seed new events.","responses":{"200":{"description":"Field-asset detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldAssetDetail"}}}},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Field asset not found."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Field asset id.","schema":{"type":"integer","format":"int64"}}]}},"/maintenance-event":{"post":{"operationId":"maintenanceEventCreate","tags":["Field maintenance"],"summary":"Open a maintenance event","description":"Opens a maintenance event against a field asset. The checklist is seeded from the taxonomy template when checklist_state is omitted; the event attributes to the authenticated user. Supply client_uuid to make the retry safe: a replay returns the event already opened rather than a second one.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEvent"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"201":{"description":"Maintenance event opened.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEvent"}}}},"409":{"description":"client_uuid already identifies another operator's event."}},"security":[{"userBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEventCreateRequest"}}}}}},"/maintenance-event/{id}":{"patch":{"operationId":"maintenanceEventUpdate","tags":["Field maintenance"],"summary":"Update a maintenance event","description":"Logs work on an open maintenance event: notes, downtime, parts cost, checklist state. Optimistically locked via row_version; a stale row_version returns 409 so the offline app can reload and merge.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEvent"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."},"409":{"description":"row_version conflict — reload and retry."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Maintenance event id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEventUpdateRequest"}}}}}},"/maintenance-event/{id}/complete":{"post":{"operationId":"maintenanceEventComplete","tags":["Field maintenance"],"summary":"Complete a maintenance event","description":"Closes a maintenance event (ended_at=NOW()). Optional downtime, parts cost and next_due_at may be supplied at close. Idempotent for an already-completed event.","responses":{"200":{"description":"Successful JSON response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEvent"}}}},"401":{"description":"Invalid or missing bearer token."},"422":{"description":"Validation failed."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Maintenance event id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceEventCompleteRequest"}}}}}},"/maintenance-event/{id}/attachment":{"post":{"operationId":"maintenanceEventAttachment","tags":["Field maintenance"],"summary":"Attach a photo or PDF to a maintenance event","description":"Uploads one image or PDF (≤8 MB) to a maintenance event. Returns the stored attachment with its mime and image/pdf flags. Supply client_uuid to make the retry safe: a replay returns the attachment already stored (deduplicated=true) rather than a second copy.","responses":{"200":{"description":"Attachment stored, or returned unchanged when this upload was already stored under the same client_uuid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceAttachmentResponse"}}}},"400":{"description":"Missing or invalid file — file required; must be an image or PDF ≤8 MB. Also returned for a malformed client_uuid."},"401":{"description":"Invalid or missing bearer token."},"404":{"description":"Maintenance event not found."},"409":{"description":"client_uuid already identifies an attachment on another event."}},"security":[{"userBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Maintenance event id.","schema":{"type":"integer","format":"int64"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Image or PDF attachment (≤8 MB)."},"caption":{"type":"string","description":"Optional attachment caption."},"client_uuid":{"type":"string","format":"uuid","description":"Client-generated UUID identifying this queued write. Replaying it returns the row already created instead of duplicating it. Optional."}},"required":["file"]}}}}}}},"components":{"securitySchemes":{"userBearer":{"type":"http","scheme":"bearer","bearerFormat":"KF50 user API token","description":"Per-user API token returned by /auth/login and refreshed by /auth/refresh."},"internalBearer":{"type":"http","scheme":"bearer","bearerFormat":"KF50_INTERNAL_KEY","description":"Shared service bearer token for trusted ingest and bridge services."}},"schemas":{"GenericObject":{"type":"object","additionalProperties":true},"Error":{"type":"object","properties":{"error":{"type":"string"},"error_code":{"type":"string"},"detail":{"type":"string"}},"additionalProperties":true},"DisabledResponse":{"type":"object","properties":{"disabled":{"type":"boolean"}},"required":["disabled"]},"AuthData":{"type":"object","description":"User auth payload returned by common\\models\\User::getAuthData().","additionalProperties":true,"properties":{"id":{"type":"integer"},"username":{"type":"string"},"email":{"type":"string","format":"email"},"token":{"type":"string"}}},"LoginRequest":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string","format":"password"}},"required":["username","password"]},"SignupRequest":{"type":"object","additionalProperties":true,"properties":{"username":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}},"RefreshRequest":{"type":"object","properties":{"authToken":{"type":"string"}}},"PasswordResetRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]},"ResetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","format":"password"}},"required":["password"]},"TechnologyLog":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"integer"},"technology_id":{"type":"integer"},"datetime":{"type":"string","format":"date-time"},"data_value":{"type":"string"}}},"TechnologyLogCreate":{"type":"object","additionalProperties":true,"properties":{"technology_id":{"type":"integer"},"datetime":{"type":"string","format":"date-time"},"data_value":{"type":"string"}}},"TechnologyLogList":{"type":"object","description":"Yii ActiveDataProvider REST list payload.","additionalProperties":true},"ValidationRules":{"type":"array","items":{"type":"array","items":{"type":"string"}}},"ChirpstackTechnologyLogRequest":{"type":"object","properties":{"deviceInfo":{"type":"object","properties":{"devEui":{"type":"string"}},"required":["devEui"]},"object":{"type":"object","additionalProperties":true},"rxInfo":{"type":"array","items":{"type":"object","properties":{"gwTime":{"type":"string","format":"date-time"}}}}},"required":["deviceInfo","object","rxInfo"]},"GeoJsonFeatureCollection":{"type":"object","properties":{"type":{"type":"string","enum":["FeatureCollection"]},"features":{"type":"array","items":{"type":"object","additionalProperties":true}},"meta":{"type":"object","additionalProperties":true}},"required":["type","features"]},"CalibrationExportRequest":{"type":"object","properties":{"run_ids":{"type":"string","description":"Comma-separated simulation run ids."}}},"VegetationHealthReport":{"type":"object","additionalProperties":true},"VegetationSeries":{"type":"object","properties":{"aoi_id":{"type":"string"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"count":{"type":"integer"},"scenes":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"VegetationIngestStatus":{"type":"object","properties":{"count":{"type":"integer"},"runs":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"IotSensorInventoryItem":{"type":"object","properties":{"device_id":{"type":"string"},"sensor_id_alias":{"type":"string","description":"Operator alias, or device_id while a discovered sensor is pending approval."},"lat":{"type":"number"},"lon":{"type":"number"},"altitude_m":{"type":"number","nullable":true},"expected_interval_s":{"type":"integer"},"deployment_state":{"type":"string"},"gateway_serial":{"type":"string","nullable":true}}},"IotGatewayInventoryItem":{"type":"object","properties":{"serial":{"type":"string"},"name_alias":{"type":"string","nullable":true},"model":{"type":"string","nullable":true},"firmware_version":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"altitude_m":{"type":"number","nullable":true},"host":{"type":"string","nullable":true},"expected_heartbeat_s":{"type":"integer"},"deployment_state":{"type":"string"}}},"IotSensorDiscoveryRequest":{"type":"object","properties":{"device_id":{"type":"string"},"gateway_serial":{"type":"string"},"first_seen_at":{"type":"string","format":"date-time"}},"required":["device_id","gateway_serial"]},"IotSensorDiscoveryResponse":{"type":"object","properties":{"device_id":{"type":"string"},"discovery_state":{"type":"string"},"created":{"type":"boolean"},"rejected":{"type":"boolean"}}},"IotGatewayStatusRequest":{"type":"object","additionalProperties":true,"properties":{"network_status":{"type":"string"},"pairing_mode":{"type":"string"},"influx_status":{"type":"string"},"mqtt_status":{"type":"string"},"mqtt_message_count":{"type":"integer","nullable":true},"local_buffer_count":{"type":"integer","nullable":true},"pending_sync_count":{"type":"integer","nullable":true},"paired_devices_count":{"type":"integer","nullable":true},"firmware_version":{"type":"string","nullable":true},"last_status_payload":{"type":"object","nullable":true,"additionalProperties":true},"last_seen_at":{"type":"string","format":"date-time"}}},"IotGatewayStatusResponse":{"type":"object","properties":{"serial":{"type":"string"},"last_seen_at":{"type":"string"}}},"IotSensorLastMetricsRequest":{"type":"object","additionalProperties":true,"properties":{"last_observation_at":{"type":"string","format":"date-time"},"last_seq_id":{"type":"integer"},"last_cycles_received":{"type":"integer"},"last_cycles_total":{"type":"integer"},"last_rssi_dbm":{"type":"number"},"last_snr_db":{"type":"number"},"last_temp_c":{"type":"number"},"last_pressure_hpa":{"type":"number"},"last_humidity_pct":{"type":"number"},"last_gas_mean_ohm":{"type":"number"},"last_gas_range_min_ohm":{"type":"number"},"last_gas_range_max_ohm":{"type":"number"},"last_profile":{"type":"string"}}},"IotSensorLastMetricsResponse":{"type":"object","properties":{"device_id":{"type":"string"},"last_observation_at":{"type":"string"}}},"IotSensorLastMetricsBulkRequest":{"type":"object","properties":{"rows":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/IotSensorLastMetricsRequest"},{"type":"object","properties":{"device_id":{"type":"string"}},"required":["device_id"]}]}}},"required":["rows"]},"IotSensorLastMetricsBulkResponse":{"type":"object","properties":{"report":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"IotSensorLogFallbackRequest":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":true}}},"required":["rows"]},"IotSensorLogFallbackResponse":{"type":"object","properties":{"written":{"type":"integer"},"duplicates":{"type":"integer"},"rejected":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"DjiDroneDiscoveryRequest":{"type":"object","properties":{"dji_serial":{"type":"string"},"dock_id":{"type":"string","nullable":true},"model":{"type":"string","nullable":true},"drone_type":{"type":"string","nullable":true,"enum":["mavic_3_enterprise","mavic_3_thermal","mavic_3_multispectral","matrice_30","matrice_30t","matrice_350_rtk","dji_dock","agras","other"]},"mission_profile":{"type":"string","nullable":true,"enum":["thermal","multispectral","orthophoto","surveillance","incident","training","other"]},"payload_profile":{"type":"string","nullable":true},"firmware_version":{"type":"string","nullable":true},"connection_mode":{"type":"string","nullable":true,"enum":["simulated","dji_cloud_api","dji_dock","manual"]},"real_connection_enabled":{"type":"boolean"},"dji_workspace_id":{"type":"string","nullable":true},"dji_device_sn":{"type":"string","nullable":true},"remote_control_sn":{"type":"string","nullable":true},"bound_dock_id":{"type":"string","nullable":true},"discovered_at":{"type":"string","format":"date-time"}},"required":["dji_serial"]},"DjiDroneDiscoveryResponse":{"type":"object","properties":{"drone_id":{"type":"integer"},"dji_serial":{"type":"string"},"discovery_state":{"type":"string","enum":["pending","approved","rejected"]},"drone_type":{"type":"string","nullable":true},"connection_mode":{"type":"string","nullable":true},"created":{"type":"boolean"}}},"DjiDroneStatusRequest":{"type":"object","properties":{"battery_pct":{"type":"integer","minimum":0,"maximum":100},"network_status":{"type":"string"},"dock_id":{"type":"string","nullable":true},"mission_id":{"type":"string","nullable":true},"observation_at":{"type":"string","format":"date-time"},"firmware_version":{"type":"string","nullable":true},"connection_mode":{"type":"string","enum":["simulated","dji_cloud_api","dji_dock","manual"]},"real_connection_enabled":{"type":"boolean"},"last_connection_check_at":{"type":"string","format":"date-time"},"last_connection_error":{"type":"string","nullable":true},"flight_plan_status":{"type":"string","enum":["draft","ready","approved","in_flight","completed","blocked"]},"orthophoto_enabled":{"type":"boolean"},"orthophoto_status":{"type":"string","enum":["not_requested","queued","processing","ready","failed"]},"orthophoto_job_id":{"type":"string","nullable":true},"orthophoto_output_url":{"type":"string","nullable":true},"media_upload_url":{"type":"string","nullable":true},"media_inventory_url":{"type":"string","nullable":true},"last_media_upload_at":{"type":"string","format":"date-time"}}},"DjiDroneStatusResponse":{"type":"object","properties":{"drone_id":{"type":"integer"},"dji_serial":{"type":"string"},"last_battery_pct":{"type":"integer","nullable":true},"last_network_status":{"type":"string","nullable":true},"last_dock_id":{"type":"string","nullable":true},"last_mission_id":{"type":"string","nullable":true},"last_observation_at":{"type":"string"},"firmware_version":{"type":"string","nullable":true},"connection_mode":{"type":"string","nullable":true},"real_connection_enabled":{"type":"boolean"},"last_connection_check_at":{"type":"string","nullable":true},"last_connection_error":{"type":"string","nullable":true},"flight_plan_status":{"type":"string","nullable":true},"orthophoto_status":{"type":"string","nullable":true},"orthophoto_job_id":{"type":"string","nullable":true},"orthophoto_output_url":{"type":"string","nullable":true},"media_upload_url":{"type":"string","nullable":true},"media_inventory_url":{"type":"string","nullable":true},"last_media_upload_at":{"type":"string","nullable":true}}},"DjiMissionStartRequest":{"type":"object","properties":{"mission_id":{"type":"string"},"dji_serial":{"type":"string"},"dock_id":{"type":"string","nullable":true},"operator_id":{"type":"integer","nullable":true},"started_at":{"type":"string","format":"date-time"},"live_stream_url":{"type":"string","nullable":true},"live_stream_expires_at":{"type":"string","format":"date-time"}},"required":["mission_id","dji_serial","started_at"]},"DjiMissionStartResponse":{"type":"object","properties":{"mission_id":{"type":"string"},"drone_id":{"type":"integer"},"status":{"type":"string"},"started_at":{"type":"string"}}},"DjiMissionEndRequest":{"type":"object","properties":{"ended_at":{"type":"string","format":"date-time"},"telemetry_count":{"type":"integer","nullable":true},"thermal_frame_count":{"type":"integer","nullable":true},"hotspot_count":{"type":"integer","nullable":true},"track_minio_url":{"type":"string","nullable":true},"mission_manifest_url":{"type":"string","nullable":true}}},"DjiMissionEndResponse":{"type":"object","properties":{"mission_id":{"type":"string"},"status":{"type":"string"},"ended_at":{"type":"string"},"telemetry_count":{"type":"integer","nullable":true},"thermal_frame_count":{"type":"integer","nullable":true},"hotspot_count":{"type":"integer","nullable":true},"track_minio_url":{"type":"string","nullable":true},"mission_manifest_url":{"type":"string","nullable":true}}},"DjiHotspotTriggerRequest":{"type":"object","properties":{"mission_id":{"type":"string"},"drone_serial":{"type":"string"},"hotspot":{"type":"object","properties":{"lat":{"type":"number"},"lon":{"type":"number"},"confidence":{"type":"number","minimum":0,"maximum":1},"temperature_max_k":{"type":"number"},"observed_at":{"type":"string","format":"date-time"},"frame_id":{"type":"string"}},"required":["lat","lon","confidence"]}},"required":["mission_id","hotspot"]},"DjiHotspotTriggerResponse":{"type":"object","properties":{"run_id":{"type":"string"},"simulation_run_id":{"type":"integer"},"mission_id":{"type":"string"}}},"PlacementPlanSummary":{"type":"object","description":"Headline fields and per-status site counts for one saved placement campaign.","properties":{"id":{"type":"integer"},"plan_uid":{"type":"string"},"label":{"type":"string"},"aoi_label":{"type":"string","nullable":true},"gateway_radius_m":{"type":"number","nullable":true},"created_at":{"type":"string"},"sites_total":{"type":"integer"},"gateways":{"type":"integer"},"sensors":{"type":"integer"},"by_status":{"type":"object","description":"lifecycle_status → count.","additionalProperties":{"type":"integer"}}}},"PlacementSite":{"type":"object","description":"One planned site in a campaign — a gateway mast or an e-nose sensor — with its as-built position once confirmed in the field.","properties":{"id":{"type":"integer"},"uid":{"type":"string"},"kind":{"type":"string","enum":["sensor","gateway"]},"rank":{"type":"integer","nullable":true},"planned":{"type":"object","description":"Planned position from the placement plan.","properties":{"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true}}},"installed":{"type":"object","nullable":true,"description":"As-built position, or null until confirmed in the field.","properties":{"lat":{"type":"number"},"lon":{"type":"number"}}},"score":{"type":"number","nullable":true},"status":{"type":"string"},"device_serial":{"type":"string","nullable":true},"accuracy_m":{"type":"number","nullable":true,"description":"As-built GPS accuracy (m) reported at confirm."},"confirmed_by":{"type":"string","nullable":true,"description":"Field operator who confirmed the install."},"assigned_to":{"type":"string","nullable":true},"install_date":{"type":"string","nullable":true},"field_notes":{"type":"string","nullable":true}}},"PlacementPlanList":{"type":"object","properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/PlacementPlanSummary"}}},"required":["plans"]},"PlacementPlanDetail":{"type":"object","properties":{"plan":{"$ref":"#/components/schemas/PlacementPlanSummary"},"sites":{"type":"array","items":{"$ref":"#/components/schemas/PlacementSite"}}},"required":["plan","sites"]},"PlacementConfirmRequest":{"type":"object","description":"As-built confirmation posted from the field app when a device is installed.","properties":{"device_serial":{"type":"string","maxLength":64,"description":"Serial scanned off the device."},"lat":{"type":"number","minimum":-90,"maximum":90,"description":"As-built latitude (WGS84)."},"lon":{"type":"number","minimum":-180,"maximum":180,"description":"As-built longitude (WGS84)."},"accuracy_m":{"type":"number","minimum":0,"maximum":100000,"description":"Optional GPS accuracy (m) from the phone at confirm time."},"confirmed_by":{"type":"string","maxLength":128,"description":"Optional field operator id/name (the confirm endpoint has no per-user identity of its own)."},"notes":{"type":"string","description":"Optional field note; appended to the site log with a [mobile] prefix."},"reassign":{"type":"boolean","description":"When the serial is already held by another site (see 409 serial_conflict), set true to move it here — the old site is released to procured and its as-built history is preserved in the audit log."}},"required":["device_serial","lat","lon"]},"PlacementConfirmResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"site":{"$ref":"#/components/schemas/PlacementSite"}}},"PlacementPhotoResponse":{"type":"object","description":"Result of a placement-site install photo upload. deduplicated=true means this client_uuid was already stored and no second copy was created.","properties":{"ok":{"type":"boolean"},"photo_id":{"type":"integer"},"url":{"type":"string"},"client_uuid":{"type":"string","nullable":true},"deduplicated":{"type":"boolean"}}},"IotSensorLookup":{"type":"object","additionalProperties":true,"description":"One device resolved by serial for a QR scan: inventory record plus live freshness. found=false (200) when the serial is unknown.","properties":{"found":{"type":"boolean"},"device_id":{"type":"string"},"alias":{"type":"string","nullable":true},"deployment_state":{"type":"string"},"discovery_state":{"type":"string"},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"altitude_m":{"type":"number","nullable":true},"expected_interval_s":{"type":"integer","nullable":true},"owner_team":{"type":"string","nullable":true},"gateway_serial":{"type":"string","nullable":true},"field_asset_id":{"type":"integer","nullable":true},"freshness":{"type":"string"},"last_observation_at":{"type":"string","nullable":true}}},"IotSensorImportRequest":{"type":"object","description":"Bulk device batch to upsert by device_id.","properties":{"default_deployment_state":{"type":"string","description":"Deployment state applied to rows that omit their own."},"approve":{"type":"boolean","description":"When true, sets discovery_state=approved so imported devices are immediately scan-resolvable."},"devices":{"type":"array","items":{"type":"object","properties":{"device_id":{"type":"string"},"sensor_id_alias":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"altitude_m":{"type":"number","nullable":true},"expected_interval_s":{"type":"integer","nullable":true},"owner_team":{"type":"string","nullable":true},"gateway_serial":{"type":"string","nullable":true},"deployment_state":{"type":"string","nullable":true}},"required":["device_id"]}}},"required":["devices"]},"IotSensorImportResponse":{"type":"object","description":"Per-row upsert report plus roll-up counts.","properties":{"report":{"type":"array","items":{"type":"object","properties":{"device_id":{"type":"string"},"status":{"type":"string","enum":["created","updated","error"]},"errors":{"type":"object","additionalProperties":true}}}},"created":{"type":"integer"},"updated":{"type":"integer"},"errors":{"type":"integer"}}},"IotSensorStatusView":{"type":"object","additionalProperties":true,"description":"Post-install \"test connection\" read: last_* snapshot, freshness, gateway link, per-metric levels and recent rows.","properties":{"device_id":{"type":"string"},"freshness":{"type":"string"},"age_s":{"type":"integer","nullable":true},"last_observation_at":{"type":"string","nullable":true},"expected_interval_s":{"type":"integer","nullable":true},"gateway_serial":{"type":"string","nullable":true},"gateway_online":{"type":"boolean","nullable":true},"metrics":{"type":"object","additionalProperties":true},"levels":{"type":"object","additionalProperties":true},"recent":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"IotGatewayStatusView":{"type":"object","description":"Already-ingested gateway health for the \"validate gateway\" step.","properties":{"serial":{"type":"string"},"network_status":{"type":"string","nullable":true},"mqtt_status":{"type":"string","nullable":true},"influx_status":{"type":"string","nullable":true},"last_seen_at":{"type":"string","nullable":true},"paired_devices_count":{"type":"integer","nullable":true},"pending_sync_count":{"type":"integer","nullable":true}}},"FieldAssetSummary":{"type":"object","additionalProperties":true,"description":"One field asset with its maintenance roll-up for the work list.","properties":{"id":{"type":"integer"},"taxonomy":{"type":"string"},"alias":{"type":"string","nullable":true},"serial":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"deployment_state":{"type":"string"},"next_due_at":{"type":"string","nullable":true},"open_events":{"type":"integer"},"overdue":{"type":"boolean"},"distance_km":{"type":"number","nullable":true}}},"FieldAssetList":{"type":"object","properties":{"assets":{"type":"array","items":{"$ref":"#/components/schemas/FieldAssetSummary"}}},"required":["assets"]},"FieldAssetDetail":{"type":"object","additionalProperties":true,"description":"A field asset with its maintenance history, photos and taxonomy checklist template.","properties":{"asset":{"$ref":"#/components/schemas/FieldAssetSummary"},"events":{"type":"array","items":{"type":"object","additionalProperties":true}},"photos":{"type":"array","items":{"type":"object","additionalProperties":true}},"checklist_template":{"type":"array","items":{"type":"object","additionalProperties":true}},"found":{"type":"boolean"}}},"MaintenanceEvent":{"type":"object","additionalProperties":true,"description":"A maintenance event against a field asset. ended_at IS NULL / is_open=true while open.","properties":{"id":{"type":"integer"},"field_asset_id":{"type":"integer"},"event_type":{"type":"string"},"severity":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"started_at":{"type":"string","nullable":true},"ended_at":{"type":"string","nullable":true},"downtime_hours":{"type":"number","nullable":true},"parts_cost":{"type":"number","nullable":true},"next_due_at":{"type":"string","nullable":true},"checklist_state":{"type":"object","nullable":true,"additionalProperties":true},"operator_user_id":{"type":"integer","nullable":true},"row_version":{"type":"integer"},"is_open":{"type":"boolean"},"client_uuid":{"type":"string","nullable":true}}},"MaintenanceEventCreateRequest":{"type":"object","description":"Open a maintenance event; checklist_state defaults to the taxonomy template when omitted.","properties":{"field_asset_id":{"type":"integer"},"event_type":{"type":"string"},"severity":{"type":"string"},"title":{"type":"string"},"notes":{"type":"string"},"started_at":{"type":"string","format":"date-time"},"next_due_at":{"type":"string","format":"date-time"},"checklist_state":{"type":"object","additionalProperties":true},"client_uuid":{"type":"string","format":"uuid","description":"Client-generated UUID identifying this queued write. Replaying it returns the row already created instead of duplicating it. Optional."}},"required":["field_asset_id","event_type"]},"MaintenanceEventUpdateRequest":{"type":"object","description":"Log work on an open event. row_version optimistically locks the row (409 on stale).","properties":{"title":{"type":"string"},"notes":{"type":"string"},"severity":{"type":"string"},"downtime_hours":{"type":"number"},"parts_cost":{"type":"number"},"checklist_state":{"type":"object","additionalProperties":true},"next_due_at":{"type":"string","format":"date-time"},"row_version":{"type":"integer","description":"The row_version the client last saw; a stale value returns 409."}}},"MaintenanceEventCompleteRequest":{"type":"object","description":"Optional fields supplied when closing a maintenance event.","properties":{"downtime_hours":{"type":"number"},"parts_cost":{"type":"number"},"next_due_at":{"type":"string","format":"date-time"}}},"MaintenanceAttachmentResponse":{"type":"object","description":"Result of a maintenance-event attachment upload. deduplicated=true means this client_uuid was already stored and no second copy was created.","properties":{"ok":{"type":"boolean"},"id":{"type":"integer"},"url":{"type":"string"},"mime":{"type":"string"},"is_image":{"type":"boolean"},"is_pdf":{"type":"boolean"},"client_uuid":{"type":"string","nullable":true},"deduplicated":{"type":"boolean"}}}}}}