agmission/Development/server/docs/Pilot_Dashboard_API.postman_collection.json

397 lines
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"info": {
"name": "AgMission — Pilot Analytics Dashboard API",
"description": "Testing collection for the Pilot Analytics Dashboard endpoints.\n\n## Setup\n1. Set `baseUrl` to your server (e.g. `https://localhost:4100`).\n2. Run **[Auth] Login** — the `jwt` variable is captured automatically.\n3. Set `tz` to your local IANA timezone (e.g. `America/Sao_Paulo`). Defaults to `UTC`.\n4. For the **Complete Job** request, set `jobId` to a job in SPRAYED (3) status owned by the logged-in applicator.\n\n## Folders\n- **[Auth]** — Login to get a JWT\n- **[Dashboard] KPI and Summary** — KPI cards and today/yesterday comparison\n- **[Dashboard] Trend** — Daily trend chart data\n- **[Dashboard] Jobs** — Active jobs panel\n- **[Dashboard] Performance** — XT error and altitude gauges\n- **[Jobs] Complete** — Transition a job from SPRAYED to COMPLETED",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "baseUrl", "value": "https://localhost:4100", "type": "string", "description": "Server base URL" },
{ "key": "jwt", "value": "", "type": "string", "description": "Captured automatically by the Login request" },
{ "key": "tz", "value": "UTC", "type": "string", "description": "IANA timezone string e.g. America/Sao_Paulo" },
{ "key": "jobId", "value": "", "type": "string", "description": "Numeric Job ID in SPRAYED (3) status — used for the Complete Job request" }
],
"item": [
{
"name": "[Auth]",
"item": [
{
"name": "Login (get JWT)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const r = pm.response.json();",
"if (r && r.token) {",
" pm.collectionVariables.set('jwt', r.token);",
" console.log('JWT captured');",
"} else {",
" console.warn('Login response did not contain a token:', JSON.stringify(r));",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"username\": \"pilot@example.com\",\n \"password\": \"yourpassword\"\n}",
"options": { "raw": { "language": "json" } }
},
"url": {
"raw": "{{baseUrl}}/api/users/login",
"host": ["{{baseUrl}}"],
"path": ["api", "users", "login"]
},
"description": "Standard AgMission login. The JWT is stored in the `jwt` collection variable and used automatically by all dashboard requests.\n\nLog in as a **Pilot** user to test dashboard read endpoints. Log in as the **Applicator** (byPuid) to test the Complete Job action."
}
}
]
},
{
"name": "[Dashboard] KPI and Summary",
"description": "KPI cards and today-vs-yesterday summary panel.",
"item": [
{
"name": "KPI Cards",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Has assignedJobs', () => pm.expect(r).to.have.property('assignedJobs'));",
"pm.test('Has assignedHectares', () => pm.expect(r).to.have.property('assignedHectares'));",
"pm.test('Has sprayedToday', () => pm.expect(r).to.have.property('sprayedToday'));",
"pm.test('Has flightHoursToday', () => pm.expect(r).to.have.property('flightHoursToday'));",
"pm.test('Has operations block', () => pm.expect(r.operations).to.have.keys(['distanceKm','sprayVolumeLiters']));",
"pm.test('Has historical block', () => pm.expect(r.historical).to.have.keys(['jobs','hectares','flightHours']));"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/kpi?tz={{tz}}",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "kpi"],
"query": [{ "key": "tz", "value": "{{tz}}", "description": "IANA timezone" }]
},
"description": "Returns all KPI card values for the authenticated pilot.\n\n**Expected fields**: `assignedJobs`, `assignedHectares`, `sprayedToday`, `flightHoursToday`, `operations.distanceKm`, `operations.sprayVolumeLiters`, `historical.jobs/hectares/flightHours` × year/month/week/day.\n\nAll numbers are rounded to 2 decimal places."
}
},
{
"name": "Daily Summary (today vs yesterday)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Has today', () => pm.expect(r).to.have.property('today'));",
"pm.test('Has yesterday', () => pm.expect(r).to.have.property('yesterday'));",
"pm.test('Has deltas', () => pm.expect(r).to.have.property('deltas'));",
"pm.test('Today has expected keys', () => pm.expect(r.today).to.have.keys(['hectares','flightHours','haPerHour','avgSpeedKmh','sprayVolumeLiters']));",
"pm.test('Deltas Pct values are null or number', () => {",
" Object.values(r.deltas).forEach(v => {",
" pm.expect(v === null || typeof v === 'number').to.be.true;",
" });",
"});"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/summary?tz={{tz}}",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "summary"],
"query": [{ "key": "tz", "value": "{{tz}}", "description": "IANA timezone" }]
},
"description": "Returns today and yesterday operational metrics with percentage change deltas.\n\n`deltas.*Pct` is `null` when yesterday value was 0 (avoids division by zero). Negative = today was worse than yesterday."
}
}
]
},
{
"name": "[Dashboard] Trend",
"description": "Daily hours and hectares chart data.",
"item": [
{
"name": "Trend — Current Week (default)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Has labels array', () => pm.expect(r.labels).to.be.an('array'));",
"pm.test('Has hoursFlown array', () => pm.expect(r.hoursFlown).to.be.an('array'));",
"pm.test('Has hectaresPerDay array', () => pm.expect(r.hectaresPerDay).to.be.an('array'));",
"pm.test('All arrays same length', () => {",
" pm.expect(r.labels.length).to.equal(r.hoursFlown.length);",
" pm.expect(r.labels.length).to.equal(r.hectaresPerDay.length);",
"});"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/trend?tz={{tz}}",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "trend"],
"query": [{ "key": "tz", "value": "{{tz}}", "description": "IANA timezone" }]
},
"description": "Returns daily `hoursFlown` and `hectaresPerDay` for the current calendar week (MonSun). All three arrays are the same length and positionally aligned."
}
},
{
"name": "Trend — Custom Date Range",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Labels match expected range length', () => {",
" pm.expect(r.labels.length).to.equal(14);",
"});"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/trend?tz={{tz}}&startDate=2026-04-16&endDate=2026-04-29",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "trend"],
"query": [
{ "key": "tz", "value": "{{tz}}", "description": "IANA timezone" },
{ "key": "startDate", "value": "2026-04-16", "description": "YYYY-MM-DD inclusive" },
{ "key": "endDate", "value": "2026-04-29", "description": "YYYY-MM-DD inclusive" }
]
},
"description": "14-day range example. Adjust `startDate`/`endDate` as needed. Maximum range is 90 days — requests exceeding that return HTTP 409."
}
},
{
"name": "Trend — Over 90 Days (expect 409)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 409 for oversized range', () => pm.response.to.have.status(409));"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/trend?tz={{tz}}&startDate=2025-01-01&endDate=2026-04-29",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "trend"],
"query": [
{ "key": "tz", "value": "{{tz}}" },
{ "key": "startDate", "value": "2025-01-01" },
{ "key": "endDate", "value": "2026-04-29" }
]
},
"description": "Validates the 90-day cap. Expects HTTP 409 `invalid_param`."
}
}
]
},
{
"name": "[Dashboard] Jobs",
"description": "Active jobs panel.",
"item": [
{
"name": "Active Jobs Panel",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Has jobs array', () => pm.expect(r.jobs).to.be.an('array'));",
"if (r.jobs.length > 0) {",
" const j = r.jobs[0];",
" pm.test('Job has jobId (number)', () => pm.expect(typeof j.jobId).to.equal('number'));",
" pm.test('Job has displayStatus', () => pm.expect(['NEW','IN_PROGRESS','COMPLETED']).to.include(j.displayStatus));",
" pm.test('Job progressPct in range 0-100', () => pm.expect(j.progressPct).to.be.within(0, 100));",
"}"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/activeJobs",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "activeJobs"]
},
"description": "Returns up to 50 jobs in statuses NEW (0) through COMPLETED (4). INVOICED (5) and ARCHIVED (9) are excluded.\n\nEach job has: `jobId` (Number), `name`, `clientName`, `aircraftReg`, `status`, `displayStatus`, `haTotal`, `haSprayed`, `progressPct`, `volumeAppliedLiters`."
}
}
]
},
{
"name": "[Dashboard] Performance",
"description": "XT error and altitude performance gauges.",
"item": [
{
"name": "Performance Gauges",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Has hasXtData', () => pm.expect(r).to.have.property('hasXtData'));",
"pm.test('Has hasAltitudeData', () => pm.expect(r).to.have.property('hasAltitudeData'));",
"pm.test('Has xtThreshold', () => pm.expect(r.xtThreshold).to.have.keys(['good','monitor']));",
"pm.test('Has altThreshold', () => pm.expect(r.altThreshold).to.have.keys(['target','goodBand','monitorBand']));",
"pm.test('Has sampleSize', () => pm.expect(r).to.have.property('sampleSize'));",
"if (r.hasXtData) {",
" pm.test('avgXtErrorMeters is a number when hasXtData', () => pm.expect(typeof r.avgXtErrorMeters).to.equal('number'));",
"}",
"if (!r.hasXtData) {",
" pm.test('avgXtErrorMeters is null when no XT data', () => pm.expect(r.avgXtErrorMeters).to.be.null);",
"}",
"if (r.hasAltitudeData) {",
" pm.test('altitudeSource is sprayHeight or radarAlt', () => pm.expect(['sprayHeight','radarAlt']).to.include(r.altitudeSource));",
"}"
]
}
}
],
"request": {
"method": "GET",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/dashboard/pilot/performance",
"host": ["{{baseUrl}}"],
"path": ["api", "dashboard", "pilot", "performance"]
},
"description": "Returns XT cross-track error and spray altitude averages based on the pilot's last 10 processed application files.\n\n`hasXtData` / `hasAltitudeData` are `false` when no sensor readings exist — render a 'No data' placeholder in that case.\n\n`altitudeSource` is `sprayHeight` (FM sensor, preferred) or `radarAlt` (AGL fallback)."
}
}
]
},
{
"name": "[Jobs] Complete",
"description": "Transition a SPRAYED job to COMPLETED. Must be logged in as the Applicator (byPuid) who owns the job.",
"item": [
{
"name": "Complete Job (happy path)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 200', () => pm.response.to.have.status(200));",
"const r = pm.response.json();",
"pm.test('Job status is now 4 (COMPLETED)', () => pm.expect(r.status).to.equal(4));"
]
}
}
],
"request": {
"method": "PATCH",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/jobs/{{jobId}}/complete",
"host": ["{{baseUrl}}"],
"path": ["api", "jobs", "{{jobId}}", "complete"]
},
"description": "Transitions job `{{jobId}}` from SPRAYED (3) to COMPLETED (4).\n\nRequirements:\n- Caller must be the Applicator who owns the job (`Job.byPuid === req.uid`).\n- Job must be in SPRAYED (3) status.\n\nSet the `jobId` collection variable to a valid numeric job ID before running."
}
},
{
"name": "Complete Job — not owner (expect 401)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 401 when not the job owner', () => pm.response.to.have.status(401));",
"const r = pm.response.json();",
"pm.test('Error tag is not_authorized', () => pm.expect(r.error['.tag']).to.equal('not_authorized'));"
]
}
}
],
"request": {
"method": "PATCH",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/jobs/{{jobId}}/complete",
"host": ["{{baseUrl}}"],
"path": ["api", "jobs", "{{jobId}}", "complete"]
},
"description": "Run this request while logged in as a **different** user than the job owner to confirm the 401 guard works. Set `jobId` to a job you do NOT own."
}
},
{
"name": "Complete Job — wrong status (expect 409)",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status is 409 for wrong job status', () => pm.response.to.have.status(409));",
"const r = pm.response.json();",
"pm.test('Error tag is status_job_invalid', () => pm.expect(r.error['.tag']).to.equal('status_job_invalid'));"
]
}
}
],
"request": {
"method": "PATCH",
"header": [{ "key": "Authorization", "value": "Bearer {{jwt}}" }],
"url": {
"raw": "{{baseUrl}}/api/jobs/{{jobId}}/complete",
"host": ["{{baseUrl}}"],
"path": ["api", "jobs", "{{jobId}}", "complete"]
},
"description": "Run this after the happy-path request has already moved the job to COMPLETED (4). The job is no longer in SPRAYED status, so you should receive 409 `status_job_invalid`."
}
}
]
}
]
}