agmission/server/docs/DASHBOARD_SNAPSHOT_DESIGN.md

281 lines
9.6 KiB
Markdown

# Dashboard Snapshot Design & Custom Params Pattern
## Overview
The **`GET /api/dashboard/pilot/snapshot`** endpoint solves the **N+1 API problem** on the frontend: instead of calling 5 separate dashboard endpoints, the frontend calls one endpoint and gets a composite response with selected modules.
## Endpoint Signature
```http
GET /api/dashboard/pilot/snapshot?include=kpi,summary,activeJobs,performance,trend&tz=UTC
```
## Query Parameters
### `include` (optional, default: all modules)
Comma-separated list of modules to fetch. Valid values:
- `kpi` — KPI card data (operations + periods)
- `summary` — Today vs yesterday deltas
- `activeJobs` — Job progress panel
- `performance` — XT error and altitude gauges
- `trend` — Trend chart data (hours + hectares over date range)
**Default behavior:** When omitted, returns **all available modules**. Recommended for initial page load.
**Example usage:**
```javascript
// Get only KPI and performance (skip activeJobs/trend)
GET /api/dashboard/pilot/snapshot?include=kpi,performance
// Get all available modules (recommended for initial load)
GET /api/dashboard/pilot/snapshot
// Get trend data for custom date range
GET /api/dashboard/pilot/snapshot?include=trend&startDate=2026-05-01&endDate=2026-05-31
```
### `tz` (optional, default: `UTC`)
IANA timezone string for date calculations (KPI periods, trend dates, etc).
### `startDate`, `endDate` (optional)
Date range for `trend` and `performance` modules (format: `YYYY-MM-DD`).
## Response Shape
Each module is optional in the response based on `include` parameter:
```json
{
"kpi": {
"operations": {
"missionsFlown": 2,
"distanceTravelledKm": 45.2,
"distanceSprayedKm": 32.1,
"sprayEfficiencyPct": 68.40,
"ferryTimePct": 31.60,
"flowAccuracyPct": 97.50,
"avgHdop": 1.20
},
"periods": {
"day": {
"assignedJobs": 3,
"assignedHectares": 45.5,
"sprayedHectares": 32.1,
"flightHours": 1.53,
"sprayEfficiencyPct": 68.40,
"ferryTimePct": 31.60,
"flowAccuracyPct": 97.50,
"avgHdop": 1.20,
"jobCounts": { "new": 0, "inProgress": 1, "completed": 2 }
},
"week": { "assignedJobs": 5, "assignedHectares": 120.3, "sprayedHectares": 98.7, "flightHours": 6.15, "sprayEfficiencyPct": 71.00, "ferryTimePct": 29.00, "flowAccuracyPct": 96.25, "avgHdop": 1.15, "jobCounts": {...} },
"month": { "assignedJobs": 12, "assignedHectares": 450.2, "sprayedHectares": 380.5, "flightHours": 19.73, "sprayEfficiencyPct": 70.50, "ferryTimePct": 29.50, "flowAccuracyPct": 98.10, "avgHdop": 1.08, "jobCounts": {...} },
"year": { "assignedJobs": 45, "assignedHectares": 1800.5, "sprayedHectares": 1520.2, "flightHours": 82.3, "sprayEfficiencyPct": 69.80, "ferryTimePct": 30.20, "flowAccuracyPct": 97.50, "avgHdop": 1.18, "jobCounts": {...} },
"all": { "assignedJobs": 120, "assignedHectares": 5200.1, "sprayedHectares": 4850.3, "flightHours": 245.1, "sprayEfficiencyPct": 69.80, "ferryTimePct": 30.20, "flowAccuracyPct": 97.50, "avgHdop": 1.18, "jobCounts": {...} }
}
},
"summary": {
"today": { "hectares": 32.1, "flightHours": 1.53, "haPerHour": 21.0, "avgSpeedKmh": 45.2, "sprayVolumeLiters": 256 },
"yesterday": {...},
"deltas": { "hectaresPct": 15, "flightHoursPct": 10, ... }
},
"activeJobs": {
"jobs": [
{ "jobId": 42, "name": "North Block", "status": 3, "displayStatus": "IN_PROGRESS", "progressPct": 75, ... }
]
},
"performance": {
"startDate": "2026-05-19",
"endDate": "2026-05-25",
"avgXtError": 2.82,
"hasXtData": true,
"xtThreshold": { "good": 1.0, "monitor": 3.0 },
"avgSprayAltitudeMeters": 3.62,
"altitudeSource": "sprayHeight",
"altThreshold": {...},
"hasAltitudeData": true,
"sampleSize": 4
},
"trend": {
"labels": ["2026-05-19", "2026-05-20", ...],
"hoursFlown": [1.53, 2.1, ...],
"hectaresPerDay": [32.1, 45.5, ...]
}
}
```
---
## Custom Params Pattern — Best Practice
### Problem
Each dashboard endpoint (`/kpi`, `/summary`, `/trend`, `/performance`) has **independent query params**:
- All accept `tz` (timezone)
- Only `trend` and `performance` accept `startDate`/`endDate`
- Parameters are **not centrally validated** — validation logic lives in each endpoint function
### Solution: No Centralized Param Validator
**Why?** Each endpoint has different requirements:
| Endpoint | Required Params | Optional Params | Logic |
|---|---|---|---|
| `/kpi` | (none) | `tz` | None — period windows are always relative (today, week, month, etc) |
| `/summary` | (none) | `tz` | Compares today vs yesterday |
| `/trend` | (none) | `tz`, `startDate`, `endDate` | Validates date range ≤ 90 days |
| `/performance` | (none) | `tz`, `startDate`, `endDate` | Validates date range ≤ 90 days; defaults to current week |
| `/snapshot` | (none) | `include`, `tz`, `startDate`, `endDate` | Routes params to appropriate sub-modules |
### Implementation Pattern in `/snapshot`
**Each module in snapshot reuses its own validation logic:**
```javascript
async function getSnapshot(req, res) {
// 1. Parse module list
const include = parseIncludeList(req.query.include);
// 2. Fetch shared data once (job/app filter)
const jobs = await fetchPilotJobs(req.uid);
const base = appMatch(jobs.map(j => j._id));
const snapshot = {};
// 3. For each module, apply its own param validation & logic
if (include.has('kpi')) {
const tz = validateTz(req.query.tz);
// KPI doesn't need startDate/endDate
snapshot.kpi = buildKpiModule(jobs, base, tz);
}
if (include.has('trend')) {
const tz = validateTz(req.query.tz);
// Trend DOES need startDate/endDate — validates date range
validateDateRange(req.query.startDate, req.query.endDate);
snapshot.trend = buildTrendModule(jobs, base, tz, startDate, endDate);
}
// 4. Return only requested modules
res.json(snapshot);
}
```
### Key Points
1. **Each endpoint owns its params:**
- No shared validator (each endpoint's logic is self-contained)
- Snapshot calls each module's validation inline
2. **Shared data fetches:**
- Pilot's jobs fetched once, not 5 times
- Application filter (`base`) reused
- No N+1 database calls
3. **Error handling:**
- Invalid `include` values → silently ignored (graceful degradation)
- Invalid date ranges → throw 409 (consistency with individual endpoints)
- Missing timezone → default to UTC (fallback in validateTz)
---
## Benefits
### Frontend Developer Experience
**Before snapshot:**
```javascript
// 5 separate requests (+ error handling for each)
const kpi = await fetch('/api/dashboard/pilot/kpi');
const summary = await fetch('/api/dashboard/pilot/summary');
const trend = await fetch('/api/dashboard/pilot/trend');
const activeJobs = await fetch('/api/dashboard/pilot/activeJobs');
const perf = await fetch('/api/dashboard/pilot/performance');
// Load states are complex: each endpoint loads independently
// Error recovery: fail gracefully per module?
// Latency: slowest endpoint dominates (serial or parallel?)
```
**After snapshot:**
```javascript
// 1 request, all modules (or just what you need)
const snapshot = await fetch('/api/dashboard/pilot/snapshot');
// or selective:
const snapshot = await fetch('/api/dashboard/pilot/snapshot?include=kpi,performance');
// Single load state, single error handler
// Latency: one network round-trip + internal parallelism
```
### Backend Performance
1. **Job fetch is 1x, not 5x**
2. **Aggregation queries parallelized** (Promise.all across periods)
3. **Database indexes reused** (same queries as individual endpoints)
4. **Bandwidth:** Omit unused modules with `?include=`
---
## Alternative Patterns (Rejected)
### ❌ Centralized Param Helper
```javascript
function parseCommonParams(req) {
return { tz: ..., startDate: ..., endDate: ... };
}
```
**Problem:** Not all endpoints use all params. Adds confusion. Snapshot needs to handle missing params per-module anyway.
### ❌ GraphQL-style Query Language
```
POST /api/dashboard/pilot/query
{ query: "{ kpi { operations periods } performance { avgXtError } }" }
```
**Problem:** Overkill for 5 modules. Complexity not justified. REST query params sufficient.
### ❌ POST with JSON body for include list
```
POST /api/dashboard/pilot/snapshot
{ "include": ["kpi", "performance"] }
```
**Problem:** GET is idempotent & cacheable. POST is not. Query params are the right tool.
---
## Testing Snapshot
### Manual test via curl
```bash
# All modules (default)
curl -H "Authorization: Bearer $TOKEN" \
'https://localhost:4100/api/dashboard/pilot/snapshot?tz=America/Toronto'
# Selective modules
curl -H "Authorization: Bearer $TOKEN" \
'https://localhost:4100/api/dashboard/pilot/snapshot?include=kpi,performance&tz=UTC'
# Trend with custom date range
curl -H "Authorization: Bearer $TOKEN" \
'https://localhost:4100/api/dashboard/pilot/snapshot?include=trend&startDate=2026-05-01&endDate=2026-05-31'
```
### Postman collection
Add snapshot test to `Pilot_Dashboard_API.postman_collection.json`:
```javascript
pm.test('snapshot includes requested modules', () => {
const res = pm.response.json();
const hasKpi = 'kpi' in res;
const hasSummary = 'summary' in res;
pm.expect(hasKpi && hasSummary).to.be.true;
});
```
---
## Future Enhancements
1. **Caching:** Cache snapshot responses per user per hour (dashboard is often static)
2. **Pagination in activeJobs:** Add `?limit=10&offset=0` to performance module
3. **Conditional module fields:** Omit fields when not needed (`?include=kpi:brief` for just operations)
4. **Batch snapshot:** `POST /api/dashboard/pilot/snapshots` with list of pilot IDs (admin view)