b654997fc9
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b654997fc9 |
Fleet sync: Municipalities + Tariffs + TariffPeriods
Mirror the customer-side rate hierarchy (Municipality → Tariff →
TariffPeriod) to the central DB, scoped by CustomerId.
Per-municipality rate structure is preserved exactly as customers
configure it locally — each tariff still references its municipality
and carries its own per-period TOU rates.
Backend
- New fleet entities: FleetMunicipality, FleetTariff, FleetTariffPeriod.
Composite PKs (CustomerId, Id) like Sites/Devices so two customers'
rate trees never collide. FleetTariff has a real FK to
FleetMunicipality on (CustomerId, MunicipalityId); FleetTariffPeriod
cascades from its FleetTariff parent.
- Two new push batch types: "municipalities" (full set) and "tariffs"
(full set with periods nested inside each tariff). Push order per
tick: sites → devices → municipalities → tariffs → measurements.
- FleetIngestService dispatches the new types. Municipalities upsert
by (CustomerId, Id). Tariffs run inside a single transaction per
batch: upsert the tariff row, DELETE all periods for that tariff,
INSERT the new set — atomic period replacement matches the
customer-side UpsertTariffRequest semantics.
- FleetPushState gains ResourceMunicipalities + ResourceTariffs
constants so the per-resource cursor/backoff state has slots for them.
- FleetQueryService.GetCustomerDetailAsync now includes municipalities
and tariffs (with periods, with municipality name joined client-side
from the lookup dict). New DTOs: FleetMunicipalityViewDto,
FleetTariffViewDto, FleetTariffPeriodViewDto.
- AdminDbContext migration AddFleetRates creates the three tables and
their indexes/FKs.
Frontend
- Customer detail page gains a "Tariffs (N)" tab. Each tariff renders
as a collapsible card with its municipality, active flag, effective
window, default rate / fixed charge / VAT, and an inline period
table (Period name, day-of-week bitmask formatted as MTWTFSS, start,
end, rate). Empty state when no tariffs synced yet.
Verified end-to-end on the dev host
- Created "Phase23 Test City" municipality + "Domestic TOU" tariff
with two periods (Peak weekdays 17:00-20:00 @ 3.75, Off-Peak
weekends 00:00-23:59 @ 1.20) on the Client.
- Within one push tick (~20s) all three rows landed on Admin:
fleet.Municipalities (1), fleet.Tariffs (1), fleet.TariffPeriods (2).
- /api/fleet/customers/{id}/detail returns the full tree with
municipality name resolved.
Tests: 61/61 still passing.
Design doc (FLEET-DESIGN.md §11) updated — tariff sync row marked
shipped, cross-customer cost compute flagged as the natural next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3333202f3a |
Dual-token rotation grace window (24h default)
Token rotation used to be immediate cutover — push gap from when ops rotates to when the customer's .env is updated and portal restarted. Now the old token keeps working for 24h after rotation, so customer ops has a full workday to swap it in without dropping a single push tick. Backend - Customer entity gains PreviousTokenHash + PreviousTokenExpiresAt (both nullable). Non-unique index on PreviousTokenHash so the OR-lookup in FindByTokenAsync stays cheap. - CustomerService.RotateTokenAsync(id, graceWindow=null, ct): copies the existing TokenHash into PreviousTokenHash with PreviousTokenExpiresAt = now + graceWindow (default 24h, lifted to CustomerService.DefaultTokenGracePeriod), then issues a new current token. Second rotation overwrites the previous slot — at most one previous token is ever honoured. - CustomerService.FindByTokenAsync matches either current OR (previous AND PreviousTokenExpiresAt > now). IsActive=false still rejects both. - DTO exposes PreviousTokenExpiresAt so the UI can render the grace window status. - New EF migration AddPreviousTokenGraceWindow on AdminDbContext. Frontend - Customers table "Token" column shows an "Old token valid until …" orange tag with a tooltip whenever the grace window is active, plus the issue/rotation date as before. - TokenShownOnceModal mentions the 24h grace window so ops knows they have time to update .env without urgency. - Rotate-token popconfirm copy updated to reflect the new behavior. Tests (+5, 61/61 passing) - CustomerTokenGraceTests covers: create doesn't set previous; rotate moves current into previous slot with future expiry; zero grace window rejects original immediately; second rotation overwrites previous (original dies, first-rotation becomes the new previous); inactive customer rejects both current AND previous. Verified end-to-end on the dev host - Migration applied cleanly on the existing admin_fleet DB (existing DEV0001 customer got NULL previous columns, no data loss). - Created GRACE01 → got token1. - Rotated → got token2. PreviousTokenExpiresAt = +24h. Both token1 and token2 push successfully (200). - Rotated again → got token3. token1 push now returns 401 (gone). token2 push still 200 (now the previous). token3 push 200 (current). Docs - FLEET-DESIGN.md §6 rewritten — no longer "immediate cutover". - §11 "open seams" row for this feature marked as shipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
880525b306 |
Add Fleet ingest design doc (portal/docs/FLEET-DESIGN.md)
Locked design for Admin / cross-customer aggregation feature. Implementation lands in phases 13-15. Key decisions captured: - Same portal binary, RunMode=Client|Admin config flag. - Two DbContext classes (ClientDbContext + AdminDbContext) to keep schemas cleanly separated and migrations sane. - Fleet ingest is opt-in (FleetIngest__Enabled=false works exactly as today, no data leaves customer stack). - Push by ReceivedAt, not Time, so firmware offline-buffer replays are picked up automatically. - Per-tick batch cap so a back-fill wave from one customer doesn't starve other customers' pushes. - SHA-256 token hash (not bcrypt) for the high-throughput ingest endpoint; tokens shown once on Admin Customers page. - Realtime continuous aggregates with wide start_offset so late back-fills materialize on the next refresh tick. - No retention policy. TimescaleDB compression on chunks older than 7 days handles long-term storage cost. - Open seams (tariff sync, RLS, GDPR delete, dual-token rotation, sharding) documented with v2 extension paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |