Mapping two Reading columns onto one meter staged two readings per row at the
same timestamp. (meter_id, time) is the reading key, so the commit failed with
a raw EF change-tracker error that named neither the column nor the meter.
- Wizard Validate() rejects duplicate Reading targets, naming the columns and
the meter. Reading-only: Delivery/TankLevel stage events, not readings.
- CommitAsync() re-validates. Previously only Preview did, so a mapping edited
after a dry run reached the database unchecked.
- ImportService.GuardDuplicateReadings() backstops the same case for the API
and any other non-wizard caller, reporting meter + date.
Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
New /import/wizard: upload an arbitrary CSV, preview the column grid, map each column to a role + target meter/category + unit, dry-run, then commit as a revertible import_batch. The /import page now lists recent imports with one-click revert. Adds CsvImporter.ReadRawRows (raw preview) and Confirm.ConfirmAsync (generic confirm). Verified end-to-end in a real browser + direct DB checks.
Claude-Session: https://claude.ai/code/session_01Kib2MniVFbD95fkgLgBBnB
Adds the "sum meter" the user asked for: a meter that doesn't physically exist
but represents the sum of other meters in the flow view.
- FlowService: a Virtual-mode meter has no readings of its own; its flow value is
the sum of its upstream meters, resolved in topological order (so "Summe Solar"
= Solar 1 + Solar 2, and Grid + Summe Solar → House with the remainder as
"Other" = export / battery / inverter losses).
- Meters editor: a hint when Mode = Virtual explaining the sum-meter behaviour.
- Reference data seeds the full demo chain: Solar 1 + Solar 2 → Summe Solar;
Netz + Summe Solar → Haus → Auto + Other — so /energy/1 shows a multi-level
Sankey out of the box.
- Fix: SankeyChart Unit was passed as the literal string "_graph.Unit" (missing
@) so node labels read "_graph.Unit" instead of "kWh". Caught by screenshotting
the live page.
Test: Virtual_sum_meter_aggregates_its_upstreams. 69 Core + 50 Integration =
119 green. Live-verified with a browser screenshot of the multi-level flow.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
Adds a meter hierarchy and a flow view: a downstream meter is a *subsection*
of an upstream one (not an addition), so you can see where a main meter's flow
divides — e.g. official water → garden, pool, other; grid/battery → all → car.
- MeterLink (schema + migration AddMeterLinks): a directed from→to flow edge.
Multi-parent allowed (a merge, e.g. grid + solar → house); multi-child is a
split. Cascade-deletes with either endpoint; unique + distinct-endpoint checks.
- FlowService: per energy type + period, builds a Sankey graph — nodes = meters
sized by consumption; link value = downstream meter's consumption, split
proportionally across multiple upstreams; unaccounted remainder under a meter
becomes a synthetic "Other" node; depth via topological longest-path.
- SankeyChart.razor: hand-rolled inline-SVG Sankey (ApexCharts has no Sankey
type) — columns by depth, nodes stacked by value, bezier ribbons sized by flow,
left→right, theme-aware, HTML-encoded labels, tooltips. Built as a MarkupString
to sidestep Razor's <text> element clash.
- /energy/{id} page (one per energy type): KPIs (consumption + cost), the flow
Sankey, and the meter list. NavMenu now lists a link per energy type
(Electricity, Water, Gas, …) loaded from the DB.
- Meters admin: cycle-safe "Sub-meter of (upstream meters)" multi-select
(descendants excluded to prevent cycles); reconciles meter_link rows on save.
- Reference data seeds a demo chain (Haus → Auto) so electricity flow shows
Haus dividing into Auto + Other.
Tests: FlowServiceTests (single-parent remainder; two-parent proportional
split); render test now asserts the flow chain + covers /energy/{id}. 69 Core +
47 Integration = 116 green. Live-verified: Haus 95,450 kWh → Auto 51,909 +
Other 43,541 (flow conserved), all 5 energy-type pages render.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
Complete the SDD §8 dashboard views that were deferred at the M5 boundary,
and fix a shipping bug that left the Docker demo empty.
Bug: "Load reference data" created meters/tank/tariffs but imported zero
readings in Docker. Root cause: sampledata/ was excluded by .dockerignore and
never copied into the build stage, so the App csproj's linked Content glob
resolved to nothing at publish time; ReferenceDataImporter then silently
skipped the missing CSVs after already writing its marker meter, leaving the
DB permanently "loaded" but empty.
- .dockerignore: stop excluding sampledata/
- Dockerfile: COPY sampledata/ into the build stage
- ReferenceDataImporter: fail-fast (validate CSVs exist before the marker
meter) and throw instead of silently skipping a missing file
- Program.cs + MeterVaultOptions: opt-in MeterVault__SeedReferenceData
(compose METERVAULT_SEED=true) for a one-command populated demo
New SDD §8 panels (read models in Infrastructure/Dashboard, Blazor pages):
- §8.4 Solar/PV (/solar): generation from GenerationCounter meters;
self-consumption / autarky % / self-consumption % / savings derived from
meters tagged total_load & grid_import via Meter.Meta role config
(MeterRoles/MeterMeta) — nothing hardcoded by name.
- §8.5 Oil/consumable (/consumables): tank level (cm→L calibrated), fill
gauge, deliveries log, burner runtime, effective L/h (fixed/empirical),
forecast-to-empty, tariff cost, monthly series.
- §8.6 Meter detail (/meters/{id}): raw readings, normalized consumption,
source status, tariff timeline, events, measured-vs-estimated markers.
- Reusable SeriesChart component; nav links; Meters list rows link to detail.
Tests: MeterMetaTests (Core, +10); DashboardRenderTests extended to assert the
three panel services compute real figures and the new routes render (108 total,
all green). Live-verified in Docker: seed imports 302 readings / 347 consumption
rows; panels render (generation 16,481 kWh, oil 3,967 L) cross-checking the DB.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
Correctness/data:
- Fix demo cost double-count: reference importer no longer imports the Kosten Strom/Wasser
columns for categories that are metered (only Heizung), so Wasser rollup is 70€ not 140€.
- Spurious-decrease guard: only a reset/swap in the window (prevReading, thisReading] explains
a decrease — an old historical reset no longer permanently disables the guard.
- Gate swap auto-detection on MappingProfile.DetectCumulativeSwaps (flag was ignored).
- Prorate basePrice by bucket length (day/month/year); guard virtual expressions against NaN/Inf.
Concurrency/infra:
- Blazor: register a DbContextFactory; CostService/DashboardService and the read pages now use
short-lived per-operation contexts (no shared circuit DbContext); guard Trends re-entrancy.
- /events: wrap event insert + consumption recompute in one transaction (atomic); 404 (not 500)
on unknown meter.
- MQTT worker: subscribe to newly-added topics on each tick; move client cleanup into finally.
- Migrations: CREATE MATERIALIZED VIEW IF NOT EXISTS + if_not_exists on CAgg/compression/
hypertable calls (re-run-safe after a mid-migration crash).
- HA worker: prune stale poll-schedule entries; export: null dangling ImportBatchIds on restore.
API/security:
- API fail-closed by default: with no keys and AllowAnonymousApi off, /api/v1 returns 401
(protects /export and /import). New MeterVault:AllowAnonymousApi opt-in.
- Cap /readings batch at 5000; report ignored (unknown-meter) count; enums as strings in JSON.
+4 regression tests (guard window, API closed, /events 404, no demo double-count). 98 tests
green; Docker deploy re-verified healthy with the API fail-closed.
Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
- German-dialect scalar parsers in Core (GermanNumber/Money/Date, ValueCell):
decimal comma, thousands dot, unit suffixes, € currency, both date shapes.
- Declarative MappingProfile + RowClassifier (skip summary/blank/all-zero rows) +
CsvImporter (CsvHelper) staging readings/events/manual-costs, with auto swap
detection on register decreases and month-end anchoring for interleaved oil dates.
- Four built-in ReferenceProfiles (Strom/Wasser/Heizöl/Kosten).
- ImportService: commit as revertible import_batch + wholesale consumption recompute
per affected meter (NormalizationService/MeterConfigFactory), revert by batch.
- Reconciliation tests: all 4 CSVs match the sheet's own columns within tolerance
(electricity 5 meters + Netz Einsparung, water swap→12, oil tank incl. deliveries +
burner hours, cost category totals). Commit/revert round-trip verified on Timescale.
69 tests green (53 Core + 16 integration).
Known follow-up (polish): historical imports can contend with the 30-day compression
policy's background job; tests pause it. Consider retry-on-deadlock or deferred
compression for large historical imports in production.
Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr