|
| 1 | +# CSAPI Server Quirks — Map Integration Debugging Session |
| 2 | + |
| 3 | +**Date:** 2026-05-10 |
| 4 | +**Status:** Complete |
| 5 | +**Scope:** Map integration defects across Go v2 and OSH servers — NIMS cameras, DSC card thumbnails, ISS photo, station images, timelapse links |
| 6 | +**Follows:** `CSAPI_Go_Server_Integration_Report_2026-04-17.md` |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1 Executive Summary |
| 11 | + |
| 12 | +During a map integration debugging session, five distinct server/client behavioral issues were uncovered. Three are server-side quirks in `connected-systems-go` (Go v2). One is a data completeness issue caused by a partial bootstrap. One is a documentation gap in the ISS system definition. All issues are now resolved with client-side workarounds and/or data patches. |
| 13 | + |
| 14 | +| # | Issue | Severity | Servers | Resolved | |
| 15 | +|---|-------|----------|---------|---------| |
| 16 | +| Q1 | Go v2 SML responses are GeoJSON Features, not raw SML objects | P2 | Go v2 | Client workaround | |
| 17 | +| Q2 | NIMS bootstrap silently incomplete — cameras never created | P2 | Go v2 | Bootstrap re-run | |
| 18 | +| Q3 | `POST /deployments/{id}/subdeployments` 400 on full body | P2 | Go v2 | Minimal-stub retry | |
| 19 | +| Q4 | OSH `system@link.href` includes `?f=json` query suffix | P3 | OSH | Strip in client | |
| 20 | +| Q5 | NIMS observation queue empty on both servers (0 published) | P2 | Both | Publisher cycle run | |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## 2 Q1 — Go v2 SML Responses Are GeoJSON Features |
| 25 | + |
| 26 | +### Symptom |
| 27 | +The Deployed System Card (DSC) showed no thumbnail, no station photo, and no SML-derived metadata (keywords, classifiers, contacts, documents) for any system on Go v2. The same systems on OSH displayed correctly. |
| 28 | + |
| 29 | +### Root Cause |
| 30 | +When fetching `GET /systems/{id}?f=application/sml+json`: |
| 31 | + |
| 32 | +- **OSH** returns a raw SML object with fields at the top level: |
| 33 | + ```json |
| 34 | + { |
| 35 | + "type": "PhysicalSystem", |
| 36 | + "label": "NDBC 44025 — Long Island, NY", |
| 37 | + "keywords": [...], |
| 38 | + "identifiers": [...], |
| 39 | + "contacts": [...], |
| 40 | + "documents": [...] |
| 41 | + } |
| 42 | + ``` |
| 43 | + |
| 44 | +- **Go v2** returns a GeoJSON Feature wrapper with SML fields inside `.properties`: |
| 45 | + ```json |
| 46 | + { |
| 47 | + "type": "Feature", |
| 48 | + "id": "3b1bc3bc-...", |
| 49 | + "geometry": { "type": "Point", "coordinates": [...] }, |
| 50 | + "properties": { |
| 51 | + "uid": "urn:os4csapi:system:...", |
| 52 | + "name": "CO-OPS 8518750 — The Battery, NY", |
| 53 | + "keywords": [...], |
| 54 | + "identifiers": [...], |
| 55 | + "contacts": [...], |
| 56 | + "documentation": [...] |
| 57 | + }, |
| 58 | + "links": [...] |
| 59 | + } |
| 60 | + ``` |
| 61 | + |
| 62 | +All `extractSml*` functions in `useDeployedSystemCard.ts` read from `sml.keywords`, `sml.documentation`, etc. These were all `undefined` on Go v2 because the actual data lives in `sml.properties.*`. |
| 63 | + |
| 64 | +Additional difference: OSH uses `documents` as the key for the documentation array. Go v2 uses `documentation`. The extractors already handled both (`sml?.documents || sml?.documentation`) — only the Feature wrapper was missed. |
| 65 | + |
| 66 | +### Fix |
| 67 | +Added a single normalization step before all SML extraction: |
| 68 | + |
| 69 | +```typescript |
| 70 | +// Normalize: Go v2 wraps SML as GeoJSON Feature (fields in .properties) |
| 71 | +// OSH returns SML with fields at top level |
| 72 | +const smlSource = systemSml?.type === 'Feature' |
| 73 | + ? (systemSml?.properties || {}) |
| 74 | + : (systemSml || {}) |
| 75 | +``` |
| 76 | + |
| 77 | +All extractors now receive `smlSource` instead of `systemSml` directly. |
| 78 | + |
| 79 | +**Commit:** `8eb8fc5` on `ogc-csapi-explorer` main |
| 80 | +**Affected systems:** Every system on Go v2 — CO-OPS stations, USGS water stations, NDBC buoys, ISS, NIMS cameras, all others. |
| 81 | + |
| 82 | +### Systems Affected by This Quirk |
| 83 | +- `card.thumbnail` — station photo (first `documentation` entry with `image/*` MIME) |
| 84 | +- `card.summarySentence` — depends on SML description |
| 85 | +- `card.kindBadge` / `card.roleBadge` — from classifiers/identifiers |
| 86 | +- `card.docsLinks` — the documentation links panel |
| 87 | +- `card.ownerMaintainer` — from contacts |
| 88 | +- `card.methodSummary` — from procedure links |
| 89 | +- Keywords, capabilities, identifiers — all derived from SML |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +## 3 Q2 — NIMS Bootstrap Silently Incomplete |
| 94 | + |
| 95 | +### Symptom |
| 96 | +NIMS cameras did not appear on the Go v2 map despite USGS water station systems being present on Go v2 and a "USGS NIMS Camera Stations" group deployment existing. |
| 97 | + |
| 98 | +### Root Cause |
| 99 | +The NIMS bootstrap (`bootstrap_usgs_nims.py`) was previously run against Go v2 and completed without errors. However it only created: |
| 100 | +- ✅ Root deployment `urn:os4csapi:deployment:usgs-nims-demo:v1` |
| 101 | +- ✅ Group deployment `urn:os4csapi:deployment:usgs-nims-cameras:v1` |
| 102 | +- ✅ Procedure `urn:os4csapi:procedure:usgs-nims-imagery:v1` |
| 103 | + |
| 104 | +It did **not** create: |
| 105 | +- ❌ 8 individual camera sub-deployments (`usgs-nims-{siteId}:v1`) |
| 106 | +- ❌ 8 `usgsNimsImage` datastreams on the water station systems |
| 107 | + |
| 108 | +**Why silently?** The map renders deployment leaf nodes based on whether the deployment has a `platform@link`. The group deployment (`usgs-nims-cameras`) has 0 sub-deployments, so nothing renders — no error, just empty. |
| 109 | + |
| 110 | +**Diagnosis commands:** |
| 111 | +```powershell |
| 112 | +# Confirmed 0 sub-deployments under the NIMS camera group |
| 113 | +GET /deployments/81bb4aa3-a428-4325-9b1d-52d7b4a84412/subdeployments |
| 114 | +# → { items: [] } |
| 115 | +
|
| 116 | +# Confirmed no usgsNimsImage datastream on water station system 09380000 |
| 117 | +GET /systems/c9d8de34-52a7-43a6-a08f-f85490d4baf5/datastreams |
| 118 | +# → 2 items: Discharge, Gage Height (no image DS) |
| 119 | +``` |
| 120 | + |
| 121 | +A dry-run confirmed what would be created: |
| 122 | +``` |
| 123 | +[DRY] Would create datastream 'usgsNimsImage' on system c9d8de34... (09380000) |
| 124 | +[DRY] Would create datastream 'usgsNimsImage' on system 9d06bc30... (09019850) |
| 125 | +... (8 total) |
| 126 | +[DRY] Would create deployment: urn:os4csapi:deployment:usgs-nims-09380000:v1 |
| 127 | +... (8 total) |
| 128 | +``` |
| 129 | + |
| 130 | +### Fix |
| 131 | +Re-ran `python -m publishers.usgs_nims.bootstrap_usgs_nims` (no `--dry-run`). All 8 datastreams and 8 camera sub-deployments created successfully. |
| 132 | + |
| 133 | +Then ran `usgs_nims_publisher --once` against Go v2 to seed initial observations (all 8 images published). |
| 134 | + |
| 135 | +### NIMS Structure on Go v2 (Post-Fix) |
| 136 | + |
| 137 | +``` |
| 138 | +53458c4f USGS NIMS Imagery Demo [root deployment] |
| 139 | +└─ 81bb4aa3 USGS NIMS Camera Stations [group] |
| 140 | + ├─ fbd0f765 NIMS Camera 09380000 [leaf, platform@link → c9d8de34] |
| 141 | + ├─ 73761560 NIMS Camera 09019850 [leaf] |
| 142 | + ├─ e0e71e45 NIMS Camera 11313433 [leaf] |
| 143 | + ├─ bd396835 NIMS Camera 08171000 [leaf] |
| 144 | + ├─ 353abfa8 NIMS Camera 01650800 [leaf] |
| 145 | + ├─ 62b191c1 NIMS Camera 05051300 [leaf] |
| 146 | + ├─ 9fffa737 NIMS Camera 12439500 [leaf] |
| 147 | + └─ 5bdf3fe4 NIMS Camera 02135000 [leaf] |
| 148 | +``` |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## 4 Q3 — `POST /deployments/{id}/subdeployments` Returns 400 with Full Body |
| 153 | + |
| 154 | +### Symptom |
| 155 | +Every `POST /deployments/{id}/subdeployments` returned HTTP 400 on the first attempt during the NIMS bootstrap. |
| 156 | + |
| 157 | +### Root Cause |
| 158 | +Go v2 rejects some fields in the deployment body when POSTing to the subdeployments endpoint. The exact offending fields are not identified in the error response (body is empty / generic 400). |
| 159 | + |
| 160 | +Bootstrap helper already implements a minimal-stub retry: |
| 161 | +```python |
| 162 | +[WARN] POST deployments/.../subdeployments failed (HTTP 400); retrying with minimal stub |
| 163 | +[OK] Created deployment urn:os4csapi:deployment:usgs-nims-09380000:v1 → id=... |
| 164 | +``` |
| 165 | + |
| 166 | +All 8 camera sub-deployments were created via the minimal stub on retry. |
| 167 | + |
| 168 | +### Behavior Details |
| 169 | +- Initial POST: full deployment body (uid, name, description, validTime, platform@link, ...) → **400** |
| 170 | +- Retry POST: minimal stub (uid, name only) → **201 Created**, empty body |
| 171 | +- After creation: full metadata PATCHed separately (when applicable) |
| 172 | + |
| 173 | +### Note |
| 174 | +This is distinct from the previously documented "201 with empty body" behavior (section 13.4 of the April report). This is a 400 on the initial POST that only affects the `/subdeployments` sub-endpoint, not the top-level `/deployments` endpoint. |
| 175 | + |
| 176 | +**Recommendation for Go server issue:** File as separate issue — "POST /deployments/{id}/subdeployments rejects valid full deployment body with 400; top-level POST /deployments accepts same body." |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## 5 Q4 — OSH `system@link.href` Includes `?f=json` Query String Suffix |
| 181 | + |
| 182 | +### Symptom |
| 183 | +When following a `system@link.href` from an OSH datastream resource to fetch the associated system, the href includes a query string: |
| 184 | + |
| 185 | +```json |
| 186 | +"system@link": { |
| 187 | + "href": "https://os4csapi-osh.duckdns.org/sensorhub/api/systems/045g?f=json" |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +Appending additional parameters (e.g., `?f=application/sml+json`) to this URL results in a malformed request: |
| 192 | +``` |
| 193 | +GET /systems/045g?f=json?f=application%2Fsml%2Bjson → 400 Bad Request |
| 194 | +``` |
| 195 | + |
| 196 | +### Root Cause |
| 197 | +OSH embeds its own internal format parameter in outbound hrefs. This is non-standard; RFC 3986 links should be canonical URLs without self-referential format hints. |
| 198 | + |
| 199 | +### Workaround |
| 200 | +Strip query string from hrefs before using them: |
| 201 | +```typescript |
| 202 | +systemId = platformLink.href.replace(/\/+$/, '').split('/').pop() || '' |
| 203 | +// Extracts ID from path, discarding any ?f=... suffix |
| 204 | +``` |
| 205 | + |
| 206 | +The Explorer already does this via the `split('/').pop()` pattern when extracting system IDs from hrefs. This works correctly. |
| 207 | + |
| 208 | +**Note:** This quirk only manifests if you try to use the full href directly rather than extracting the ID. Clients that compose their own request URLs (using the extracted ID) are not affected. |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## 6 Q5 — NIMS Observations Missing on Both Servers (0 Published) |
| 213 | + |
| 214 | +### Symptom |
| 215 | +NIMS image datastreams existed on both Go v2 and OSH but had 0 observations. The map marker and metadata card showed no camera image, no timelapse link. |
| 216 | + |
| 217 | +### Root Cause |
| 218 | +The `usgs-nims-publisher.service` systemd service targets OSH by default (via the `.env` file). On Go v2, a separate `usgs-nims-publisher-go.service` should exist (see Appendix A of April report). The Go v2 publisher was not running or had failed silently. |
| 219 | + |
| 220 | +On OSH, the publisher had also failed to post new observations — likely a crash after the server migration to the new URL/auth config or a transient network issue. |
| 221 | + |
| 222 | +### Immediate Fix |
| 223 | +Ran one-shot publishes manually: |
| 224 | + |
| 225 | +```powershell |
| 226 | +# Go v2 |
| 227 | +$env:OSH_BASE_URL = "https://129-80-248-53.sslip.io/csapi-go-v2" |
| 228 | +python -m publishers.usgs_nims.usgs_nims_publisher --once |
| 229 | +# → Published: 8 |
| 230 | +
|
| 231 | +# OSH |
| 232 | +$env:OSH_BASE_URL = "https://129-80-248-53.sslip.io/sensorhub/api" |
| 233 | +python -m publishers.usgs_nims.usgs_nims_publisher --once |
| 234 | +# → Published: 8 |
| 235 | +``` |
| 236 | + |
| 237 | +### NIMS Observation Result Schema |
| 238 | + |
| 239 | +For reference, a NIMS observation result from this publisher: |
| 240 | +```json |
| 241 | +{ |
| 242 | + "camId": "AZ_Colorado_River_at_Lees_Ferry_Upstream", |
| 243 | + "filename": "AZ_Colorado_River_at_Lees_Ferry_Upstream___2026-05-10T23-00-05Z.jpg", |
| 244 | + "imageUrl": "https://usgs-nims-images.s3.amazonaws.com/overlay/AZ_Colorado_River_at_Lees_Ferry_Upstream/...jpg", |
| 245 | + "smallUrl": "https://usgs-nims-images.s3.amazonaws.com/720/AZ_Colorado_River_at_Lees_Ferry_Upstream/...jpg", |
| 246 | + "thumbUrl": "https://usgs-nims-images.s3.amazonaws.com/thumbnail/AZ_Colorado_River_at_Lees_Ferry_Upstream/...jpg", |
| 247 | + "mediaType": "image/jpeg", |
| 248 | + "stationId": "09380000", |
| 249 | + "timestamp": "2026-05-10T23:00:05Z", |
| 250 | + "timeLapseUrl": "https://usgs-nims-images.s3.amazonaws.com/timelapse/AZ_Colorado_River_at_Lees_Ferry_Upstream/...mp4" |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +The DSC card (`useDeployedSystemCard.ts`) reads these fields: |
| 255 | +- `result.imageUrl` → `card.cameraImageUrl` |
| 256 | +- `result.thumbUrl` → `card.cameraThumbUrl` |
| 257 | +- `result.timeLapseUrl` → `card.cameraTimeLapseUrl` |
| 258 | +- `result.camId` → `card.cameraCamId` |
| 259 | +- `result.mediaType` → gate condition (must start with `image/`) |
| 260 | + |
| 261 | +**NDBC BuoyCAM result schema** (for comparison): |
| 262 | +```json |
| 263 | +{ |
| 264 | + "stationId": "46013", |
| 265 | + "imageUrl": "https://os4csapi-osh.duckdns.org/buoycam/46013/2026/04/17/...jpg", |
| 266 | + "mediaType": "image/jpeg", |
| 267 | + "cameraStatus": "ok", |
| 268 | + "sha256": "...", |
| 269 | + "contentLength": 59812.0, |
| 270 | + "latestImageUrl": "https://www.ndbc.noaa.gov/buoycam.php?station=46013" |
| 271 | +} |
| 272 | +``` |
| 273 | + |
| 274 | +BuoyCAM does **not** have `timeLapseUrl`. The timelapse link only appears for NIMS cameras. |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## 7 Supplementary: ISS System Missing Documentation/Photo |
| 279 | + |
| 280 | +Not a server quirk per se — a data gap in the ISS bootstrap. |
| 281 | + |
| 282 | +### Symptom |
| 283 | +The ISS DSC card showed no thumbnail. After Q1 was fixed (SML Feature normalization), the card could theoretically show a photo if one existed in the system's `documentation` array. The ISS position system had none. |
| 284 | + |
| 285 | +### Fix |
| 286 | +Added `documentation` array to `_system_position()` in `bootstrap_iss.py`: |
| 287 | +```python |
| 288 | +"documentation": [ |
| 289 | + { |
| 290 | + "role": "http://dbpedia.org/resource/Photograph", |
| 291 | + "name": "ISS Photograph", |
| 292 | + "link": { |
| 293 | + "href": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/International_Space_Station_after_undocking_of_STS-132.jpg/640px-International_Space_Station_after_undocking_of_STS-132.jpg", |
| 294 | + "type": "image/jpeg", |
| 295 | + }, |
| 296 | + }, |
| 297 | + { |
| 298 | + "role": "http://dbpedia.org/resource/Web_page", |
| 299 | + "name": "ISS Tracking Page", |
| 300 | + "link": { |
| 301 | + "href": "https://spotthestation.nasa.gov/", |
| 302 | + "type": "text/html", |
| 303 | + }, |
| 304 | + }, |
| 305 | +] |
| 306 | +``` |
| 307 | + |
| 308 | +**Patched live** on both servers: |
| 309 | +- Go v2 ISS position system: `8f06cdeb-d50e-4aca-baeb-5aa5601323ff` → PUT 204 |
| 310 | +- OSH ISS position system: `04i0` → PUT 204 |
| 311 | + |
| 312 | +--- |
| 313 | + |
| 314 | +## 8 Updated Behavioral Comparison Table |
| 315 | + |
| 316 | +Extends the table from the April 2026 report (section 13.7). |
| 317 | + |
| 318 | +| Behavior | SensorHub (OSH) | connected-systems-go (Go v2) | |
| 319 | +|---|---|---| |
| 320 | +| SML response format (`?f=application/sml+json`) | Raw SML object (top-level fields) | **GeoJSON Feature** (SML fields in `.properties`) | |
| 321 | +| SML response key for documentation | `documents` | `documentation` | |
| 322 | +| `system@link.href` format | Contains `?f=json` suffix | Clean path, no suffix | |
| 323 | +| POST `/deployments/{id}/subdeployments` with full body | Accepts | **Returns 400**; minimal stub retry succeeds | |
| 324 | +| `?f=` parameter for SML format | `?f=sml3` | `?f=application/sml%2Bjson` | |
| 325 | +| `outputName` filter on `/datastreams` | **Not reliable** (returns wrong items) | Works correctly | |
| 326 | +| Observation default sort | Ascending | Descending (newest-first) | |
| 327 | +| `resultTime=latest` param | Honored | **Silently ignored** | |
| 328 | +| `/deployments/{id}/systems` endpoint | Supported | **404 Not Found** | |
| 329 | +| POST 201 response body | Full resource JSON | Empty; `Location` header only | |
| 330 | + |
| 331 | +--- |
| 332 | + |
| 333 | +## 9 Server State After This Session |
| 334 | + |
| 335 | +### Go v2 (`https://129-80-248-53.sslip.io/csapi-go-v2`) |
| 336 | + |
| 337 | +| Resource Type | Count | Notes | |
| 338 | +|---|---|---| |
| 339 | +| Systems | ~37 | Unchanged | |
| 340 | +| Datastreams | +8 | Added `usgsNimsImage` for 8 NIMS camera stations | |
| 341 | +| Deployments | +10 | Added 8 NIMS camera leaf deployments (+ 2 already existed) | |
| 342 | +| Observations | +8 | Initial NIMS image observations published | |
| 343 | + |
| 344 | +### OSH (`https://129-80-248-53.sslip.io/sensorhub/api`) |
| 345 | + |
| 346 | +| Resource Type | Count | Notes | |
| 347 | +|---|---|---| |
| 348 | +| Systems | ~44 | Unchanged | |
| 349 | +| Datastreams | Unchanged | NIMS datastreams already existed | |
| 350 | +| Observations | +8 | NIMS image observations published (were stale/empty) | |
| 351 | + |
| 352 | +### System Patches Applied (Both Servers) |
| 353 | + |
| 354 | +| System | Server | Change | |
| 355 | +|---|---|---| |
| 356 | +| ISS Position Publisher (`iss-position-publisher:v1`) | Go v2 | Added `documentation` with photo + tracking page | |
| 357 | +| ISS Position Publisher (`iss-position-publisher:v1`) | OSH | Added `documentation` with photo + tracking page | |
| 358 | + |
| 359 | +--- |
| 360 | + |
| 361 | +## 10 Client-Side Fixes Committed |
| 362 | + |
| 363 | +| File | Change | |
| 364 | +|---|---| |
| 365 | +| `demo/src/composables/useDeployedSystemCard.ts` | Normalize SML source: detect Go v2 Feature wrapper, extract `.properties` before all `extractSml*` calls | |
| 366 | +| `publishers/iss/bootstrap_iss.py` | Add `documentation` array (photo + tracking page) to `_system_position()` | |
| 367 | + |
| 368 | +**Commit:** `8eb8fc5` — `ogc-csapi-explorer` main — deployed via Cloudflare auto-build. |
| 369 | + |
| 370 | +--- |
| 371 | + |
| 372 | +## 11 Recommended Go Server Issues to File |
| 373 | + |
| 374 | +These are new findings beyond the April report's outstanding list: |
| 375 | + |
| 376 | +1. **`POST /deployments/{id}/subdeployments` rejects full body with 400** — body content vs. top-level endpoint inconsistency. Minimal stub workaround exists but forces a separate PATCH round-trip. |
| 377 | + |
| 378 | +2. **SML responses from `GET /systems/{id}?f=application/sml+json` return GeoJSON Feature wrapper instead of raw SML** — breaks any client expecting SML field names at the top level of the response. OSH returns correct raw SML format. |
| 379 | + |
| 380 | +3. **`usgs-nims-publisher-go.service` may not be running** — verify systemd service status on the VM; the publisher had 0 observations on Go v2 before manual re-run. |
0 commit comments