feat(hotspots): ingest live SiPongi hotspots with clustering and hover cards (Issue 3) #12

Merged
yuris merged 1 commit from feat/issue-3-sipongi-hotspots into main 2026-10-14 20:01:52 +00:00
Owner

Summary

Implements Issue 3: Live SiPongi active hotspots ingestion and clustering.

  • Created /api/hotspots API route proxying KLHK SiPongi+ active fires with gzip decompression and 15-minute server caching (s-maxage=900).
  • Modeled HotspotFeatureCollection and HotspotProperties with normalized coordinates (5 decimal places), formatted timestamps (WIB and UTC), and confidence levels.
  • Configured MapLibre dynamic clustering with scaled circle radii and colors (<10: amber, 10-50: orange, >50: red).
  • Implemented cluster click expansion (getClusterExpansionZoom).
  • Implemented unclustered glowing confidence circles with interactive hover cards (HotspotHoverCard) displaying satellite, province, district, village, and timestamps.
  • Added live hotspots toggle switch and confidence legend to LayerToggles.

Closes #3

## Summary Implements Issue 3: Live SiPongi active hotspots ingestion and clustering. - Created `/api/hotspots` API route proxying KLHK SiPongi+ active fires with gzip decompression and 15-minute server caching (`s-maxage=900`). - Modeled `HotspotFeatureCollection` and `HotspotProperties` with normalized coordinates (5 decimal places), formatted timestamps (WIB and UTC), and confidence levels. - Configured MapLibre dynamic clustering with scaled circle radii and colors (`<10`: amber, `10-50`: orange, `>50`: red). - Implemented cluster click expansion (`getClusterExpansionZoom`). - Implemented unclustered glowing confidence circles with interactive hover cards (`HotspotHoverCard`) displaying satellite, province, district, village, and timestamps. - Added live hotspots toggle switch and confidence legend to `LayerToggles`. Closes #3
yuris-bot approved these changes 2026-10-14 20:01:13 +00:00
yuris-bot left a comment

Review: Issue 3 - Live SiPongi active hotspots ingestion and clustering

Verdict: APPROVE


Five-Axis Review Summary

1. Correctness

  • Upstream Ingestion & Normalization: Successfully tested live ingestion from KLHK SiPongi+, fetching 10,334 live hotspots. Gzip decompression (0x1f 0x8b header detection) and JSON parsing work reliably. Coordinates correctly normalized to 5 decimal places and filtered to Indonesia bounds.
  • Clustering: MapLibre clustering correctly groups nearby hotspots into amber/orange/red circles according to hotspot count brackets (<10, 10–50, >50).
  • Cluster Expansion: Clicking a cluster calculates getClusterExpansionZoom and smoothly zooms into constituent points.
  • Hover Card: Rich popups display sensor, location hierarchy, timestamps in WIB and UTC, and confidence ratings.

2. Readability & Simplicity

  • Domain models and types well structured in src/types/hotspot.ts.
  • Ingestion logic isolated in src/lib/services/sipongi.ts.
  • MapLibre circle and symbol layer specifications separated cleanly in hotspots-layer.tsx.

3. Architecture

  • Clean proxy architecture in src/app/api/hotspots/route.ts with in-memory caching (15-minute TTL) and stale-while-revalidate fallback if upstream is unreachable.
  • Layer toggle state in page.tsx cleanly propagates to MapView without canvas recreation.

4. Security & Hardening

  • Input validation: late query parameter validated and clamped (1–72 hours).
  • Upstream protection: 20-second timeout via AbortController prevents hanging serverless instances.
  • Zero secrets leaked.

5. Performance

  • 10,334 raw points aggregated via WebGL clustering on the GPU, avoiding CPU canvas degradation.
  • Server-side response caching with X-Cache: HIT/MISS/STALE headers.

Considerations for Polish (Non-blocking)

  1. React Root Cleanup on Hover:
    In src/components/map/layers/hotspots-layer.tsx:158, createRoot(popupNode) is called on every hotspot mouseenter. On mouseleave, activePopup.remove() removes the DOM node, but root.unmount() is not called. Reusing a persistent root or tracking let activeRoot: Root | null = null with activeRoot?.unmount() will prevent detached React root memory retention during rapid map hovering.
  2. Reduced Motion:
    In src/components/map/popups/hotspot-hover-card.tsx:41, consider replacing animate-ping with motion-safe:animate-pulse to respect user motion preferences.

Ponytail Audit

  • Dependencies: No external packages added; uses native node:zlib (gunzipSync) and existing maplibre-gl.
  • Complexity: Zero bloat. Lean, functional code.

Verification

  • vp check: All 83 files formatted; 0 lint errors, 0 type errors.
  • next build: Standalone production build succeeded in 9.3s with /api/hotspots registered.
  • Upstream live test verified (10,334 points ingested and normalized).
# Review: Issue 3 - Live SiPongi active hotspots ingestion and clustering ### Verdict: **APPROVE** --- ### Five-Axis Review Summary #### 1. Correctness - **Upstream Ingestion & Normalization**: Successfully tested live ingestion from KLHK SiPongi+, fetching 10,334 live hotspots. Gzip decompression (`0x1f 0x8b` header detection) and JSON parsing work reliably. Coordinates correctly normalized to 5 decimal places and filtered to Indonesia bounds. - **Clustering**: MapLibre clustering correctly groups nearby hotspots into amber/orange/red circles according to hotspot count brackets (<10, 10–50, >50). - **Cluster Expansion**: Clicking a cluster calculates `getClusterExpansionZoom` and smoothly zooms into constituent points. - **Hover Card**: Rich popups display sensor, location hierarchy, timestamps in WIB and UTC, and confidence ratings. #### 2. Readability & Simplicity - Domain models and types well structured in `src/types/hotspot.ts`. - Ingestion logic isolated in `src/lib/services/sipongi.ts`. - MapLibre circle and symbol layer specifications separated cleanly in `hotspots-layer.tsx`. #### 3. Architecture - Clean proxy architecture in `src/app/api/hotspots/route.ts` with in-memory caching (15-minute TTL) and stale-while-revalidate fallback if upstream is unreachable. - Layer toggle state in `page.tsx` cleanly propagates to `MapView` without canvas recreation. #### 4. Security & Hardening - Input validation: `late` query parameter validated and clamped (1–72 hours). - Upstream protection: 20-second timeout via `AbortController` prevents hanging serverless instances. - Zero secrets leaked. #### 5. Performance - 10,334 raw points aggregated via WebGL clustering on the GPU, avoiding CPU canvas degradation. - Server-side response caching with `X-Cache: HIT/MISS/STALE` headers. --- ### Considerations for Polish (Non-blocking) 1. **React Root Cleanup on Hover**: In `src/components/map/layers/hotspots-layer.tsx:158`, `createRoot(popupNode)` is called on every hotspot `mouseenter`. On `mouseleave`, `activePopup.remove()` removes the DOM node, but `root.unmount()` is not called. Reusing a persistent root or tracking `let activeRoot: Root | null = null` with `activeRoot?.unmount()` will prevent detached React root memory retention during rapid map hovering. 2. **Reduced Motion**: In `src/components/map/popups/hotspot-hover-card.tsx:41`, consider replacing `animate-ping` with `motion-safe:animate-pulse` to respect user motion preferences. --- ### Ponytail Audit - **Dependencies**: No external packages added; uses native `node:zlib` (`gunzipSync`) and existing `maplibre-gl`. - **Complexity**: Zero bloat. Lean, functional code. --- ### Verification - `vp check`: All 83 files formatted; 0 lint errors, 0 type errors. - `next build`: Standalone production build succeeded in 9.3s with `/api/hotspots` registered. - Upstream live test verified (10,334 points ingested and normalized).
yuris merged commit 249588141f into main 2026-10-14 20:01:52 +00:00
Sign in to join this conversation.
No reviewers
No labels
backend
frontend
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/titik-gambut-next!12
No description provided.