281 lines
9.6 KiB
Markdown
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)
|