feat(ingest): FleetPulse ingest, presence projector, P12 and heatmap component #85

Merged
patrick merged 3 commits from feature/ingest-fleetpulse into main 2026-09-11 13:41:10 +00:00
Owner

Description

Vehicles now get their expected weekly presence pattern into Polaris automatically: a FleetPulse feed document posted to the API is checked for duplicates, validated, matched to an existing vehicle (never auto-created), and used to keep that vehicle's sites, presence pattern and suggested service suitability up to date — all without ever discarding history. From that pattern, the system works out concrete dated time windows a vehicle is expected at each site, correctly handling a pattern's fixed weekly cycle even across a daylight-saving clock change and a block that runs to midnight. Admins get a page to see the feed log, replay a document by hand, or paste/drop one in manually — including a shortcut to create a missing vehicle and immediately re-ingest a feed that was rejected for it. A first building block for showing a vehicle's pattern as a weekday × hour heatmap is also delivered, ready for the vehicle detail page to use once it exists.

Closes #17 (PH2 / T-B). Also resolves the two small cross-boundary fixes flagged in seam-request #84 (filed from this PR).

Scenarios: S01, S02, S12, S16. Edge cases: E05, E06, E17.

Type of Change

  • New feature

Breaking Changes

None for any already-merged consumer. polaris.api.dto.ingest's FleetPulseDocument shape changes (see Additional Context) — no existing client depended on it, since this is the first ticket to actually post to POST /ingest/fleetpulse for real.

Test Plan

Backend: ruff, mypy --strict, pytest (421 passed, includes new unit coverage for the ingest pipeline's validation/geometry/hashing helpers and the projector's DST/24:00/observation-group/gap-splitting behaviour, plus integration coverage against a real Postgres for idempotency, unknown-vehicle rejection, malformed documents, site versioning, suitability suggestions and replay). Frontend: ng lint, ng build, npm test (76 passed, includes the new ingest store and heatmap component), npm run i18n:parity.

Checklist

  • Conventional commit(s), one logical change per commit
  • Tests added/updated and passing
  • docs/ updated for any behavioural change (docs/03-scenarios.md §S01–S02)
  • No hard deletes; no direct status updates outside polaris.domain
  • No business logic in routers/components
  • Migration included if the schema changed — not needed, no schema change
  • No new dependency without a reason in the commit body — none added

Additional Context

  • polaris.api.dto.ingest's FleetPulseDocument/FleetPulseSite/FleetPulseTimeBlock were corrected to match the real FleetPulse feed shape (confirmed against docs/design_document.md and the sample feed from #14) instead of the shape that module's own docstring already flagged as inferred pending this reconciliation. backend/openapi.json regenerated in the same commit.
  • The heatmap component is built and tested but not yet wired into an actual vehicle detail page — that page is still a placeholder owned by the master-data track. It is designed to be dropped in directly once that page exists.
  • The presence-confidence adjustment hook's formula is implemented directly (it is already fully specified in the design docs) rather than waiting on the dedicated scorer ticket that owns its eventual file — the call site can swap in that ticket's implementation later with no change needed here. Full reasoning in polaris/projector/core.py.
  • Seam-request #84 has the detail on all three small cross-boundary fixes this PR makes directly (a stale DTO shape, a now-outdated shared stub test, and a missing lint ignore for the generated API client) rather than leaving the ticket blocked.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX

## Description Vehicles now get their expected weekly presence pattern into Polaris automatically: a FleetPulse feed document posted to the API is checked for duplicates, validated, matched to an existing vehicle (never auto-created), and used to keep that vehicle's sites, presence pattern and suggested service suitability up to date — all without ever discarding history. From that pattern, the system works out concrete dated time windows a vehicle is expected at each site, correctly handling a pattern's fixed weekly cycle even across a daylight-saving clock change and a block that runs to midnight. Admins get a page to see the feed log, replay a document by hand, or paste/drop one in manually — including a shortcut to create a missing vehicle and immediately re-ingest a feed that was rejected for it. A first building block for showing a vehicle's pattern as a weekday × hour heatmap is also delivered, ready for the vehicle detail page to use once it exists. ## Related Issues Closes #17 (PH2 / T-B). Also resolves the two small cross-boundary fixes flagged in seam-request #84 (filed from this PR). Scenarios: S01, S02, S12, S16. Edge cases: E05, E06, E17. ## Type of Change - [x] New feature ## Breaking Changes None for any already-merged consumer. `polaris.api.dto.ingest`'s `FleetPulseDocument` shape changes (see Additional Context) — no existing client depended on it, since this is the first ticket to actually post to `POST /ingest/fleetpulse` for real. ## Test Plan Backend: `ruff`, `mypy --strict`, `pytest` (421 passed, includes new unit coverage for the ingest pipeline's validation/geometry/hashing helpers and the projector's DST/24:00/observation-group/gap-splitting behaviour, plus integration coverage against a real Postgres for idempotency, unknown-vehicle rejection, malformed documents, site versioning, suitability suggestions and replay). Frontend: `ng lint`, `ng build`, `npm test` (76 passed, includes the new ingest store and heatmap component), `npm run i18n:parity`. ## Checklist - [x] Conventional commit(s), one logical change per commit - [x] Tests added/updated and passing - [x] `docs/` updated for any behavioural change (docs/03-scenarios.md §S01–S02) - [x] No hard deletes; no direct status updates outside `polaris.domain` - [x] No business logic in routers/components - [x] Migration included if the schema changed — not needed, no schema change - [x] No new dependency without a reason in the commit body — none added ## Additional Context - `polaris.api.dto.ingest`'s `FleetPulseDocument`/`FleetPulseSite`/`FleetPulseTimeBlock` were corrected to match the real FleetPulse feed shape (confirmed against `docs/design_document.md` and the sample feed from #14) instead of the shape that module's own docstring already flagged as inferred pending this reconciliation. `backend/openapi.json` regenerated in the same commit. - The heatmap component is built and tested but not yet wired into an actual vehicle detail page — that page is still a placeholder owned by the master-data track. It is designed to be dropped in directly once that page exists. - The presence-confidence adjustment hook's formula is implemented directly (it is already fully specified in the design docs) rather than waiting on the dedicated scorer ticket that owns its eventual file — the call site can swap in that ticket's implementation later with no change needed here. Full reasoning in `polaris/projector/core.py`. - Seam-request #84 has the detail on all three small cross-boundary fixes this PR makes directly (a stale DTO shape, a now-outdated shared stub test, and a missing lint ignore for the generated API client) rather than leaving the ticket blocked. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
Implements S01/S02/S12 (docs/03-scenarios.md): content-hash idempotency,
schema validation (structural at the API boundary, semantic — polygon
closed, HH:MM/24:00 parsing, single vehicle_id per document — in the
pipeline so a rejection still produces an audit-trail ingest_record),
unknown-vehicle rejection, site upsert with valid_from/valid_to
versioning, append-only presence-block replace-per-ingest, and
potential_services -> site_suitability suggestions that never overwrite
an existing row.

polaris.projector.core is a pure module (core/contracts only, enforced
by tests/unit/test_import_rules.py): a 15-minute wall-clock slot
timeline built across the whole horizon so cross-midnight joins (E05)
and DST transitions (E06) both fall out of "find contiguous runs"
without special-casing. worker/handlers/project_windows.py builds
ProjectionInput from the DB and persists windows, superseding old ones
except any a published assignment still references.

Corrects polaris.api.dto.ingest's FleetPulseDocument/FleetPulseSite/
FleetPulseTimeBlock to match the real feed shape (confirmed against
docs/design_document.md and the #14 sample feed) instead of the shape
PH1.7 had inferred pending this reconciliation, per that module's own
docstring; backend/openapi.json regenerated in this commit. Updates
tests/unit/api/routers/plans_locks_assignments_stub_test.py, whose
ingest-endpoint stub assertions this makes stale. Both are flagged in
seam-request issue #84 per the precedent in issues #52/#57/#66.

Closes #17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
P12 (docs/06-ux.md): feed log (vehicle, received, hash, status, error),
manual upload (paste or drop a .json file), replay, and a
create-vehicle-and-re-ingest shortcut on rejected-unknown-vehicle rows.
IngestStore composes one signalStoreFeature slice (list/upload/replay
are one cohesive concern, not the independent ones the slice pattern
targets). nl/en parity.

features/vehicles/components/heatmap.component.ts is the P06 heatmap
component only (dumb, input()-only, no store/HTTP) per the ticket's
"heatmap is a separate component file" note — it is not wired into an
actual P06 page, since that page is still T-A's NotBuiltPage
placeholder; every user-facing string is a caller-supplied,
already-translated input (the AppButtonComponent.label pattern) rather
than the component owning a Transloco scope, since
features/vehicles/i18n/** is outside this ticket's owned paths.

Adds an eslint ignore for core/api/generated/** (openapi-typescript's
output trips @typescript-eslint/consistent-indexed-object-style by the
hundreds the moment backend/openapi.json exists to generate a client
from, which this ticket is the first to hit) — flagged in seam-request
issue #84.

Part of #17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
patrick force-pushed feature/ingest-fleetpulse from f0de2054fe to f7fc44ce5b 2026-09-11 13:39:55 +00:00 Compare
patrick deleted branch feature/ingest-fleetpulse 2026-09-11 13:41:10 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
patrick/Polaris!85
No description provided.