Analysis: one selected period, one set of numbers, on every page
ci / build-test (push) Successful in 2m31s

The dashboards told several stories at once. Overview asked for full
calendar years, meter detail for a fixed 12-month window that was really
13, Trends for 24 months with an Apply button, and the energy pages for
60. Each page derived "today" from UTC, so the first hours of a local day
belonged to yesterday. A missing tariff, a month nobody measured and a
genuine zero all rendered as 0. And a virtual meter -- the one thing the
spreadsheet leans on hardest -- was excluded from analysis outright:
MeterPeriodService returned null for it and the page offered a flow
diagram instead.

docs/DASHBOARD_ANALYSIS_CHANGE_BRIEF.md is the work order. Every choice it
left open is settled in docs/ANALYSIS_IMPLEMENTATION_NOTE.md as D-01..D-58
plus amendments A-01..A-30; code, tests and release notes cite those ids.

The analysis layer

Core/Analysis holds the pure rules: period presets resolved once in the
instance zone into a local date range and a half-open UTC range, bucket
plans, calendar-unit comparisons, coverage runs with a resolution class,
normalized quantities and units, the totals policy, the virtual formula
parser/validator/evaluator, and the cost calculator. "Now" comes from
TimeProvider; services never read the clock.

Normalization now writes, in the same transaction as consumption and by
diff, per-meter rollups by local day and month plus coverage runs and a
rollup state (AnalysisDataWriter). AnalysisReader answers a request from
those tables -- month rollups for month and year buckets, day rollups
otherwise, at most two partial edge days from consumption -- and
CostReader prices the result month by month. Pages, /api/v1 and the CSV
export read nothing else. The unused continuous aggregates are dropped.

The reader's statement count per request is constant whether it covers one
meter or a thousand. On a synthetic 1,000-meter, ten-year instance the
brief's target request (100 meters, ten years, monthly) takes 374 ms
against a two-second target, and the Overview went from 48,244 SQL
statements per load to 205.

Missing is not zero

Every bucket carries a status -- available, partial, missing, unresolved,
invalid, pending -- derived from coverage, never from the amount, with
provenance and a reason code beside it. A true zero is a number and a bar
on the baseline; an unknown bucket is a gap that says why; a month whose
data only exists monthly says so instead of inventing daily detail; a
scope with no tariff says "not priced" instead of 0. Rows whose interval
closes after now are reported separately rather than counted.

Virtual meters are analysis subjects

A virtual meter stores a canonical definition -- expression over m<id>
references, result kind, unit and cost rule -- validated on save and on
read for syntax, unknown or self references, loops and unit/kind rules.
It is evaluated on read from its sources' rollups over their joint
coverage: a missing source makes the bucket missing, an observed zero is
a valid input, a non-finite result is invalid with its dependency path,
and the page lists each source's contribution. Topology links are
topology only and never rewrite a saved calculation; expression-less
meters from older installs are converted once at startup. The editor has
Sum, Difference and Advanced modes with a live preview.

Totals and the bill

Per energy type the totals policy separates use, grid import, export,
generation and runtime, marks breakdown meters as breakdowns and virtual
meters as views, and never adds across units. The bill follows it: grid
import where there is one, separately priced subsections at their own
price, feed-in only on export meters, standing charges once per scope per
local day, manual costs once on their start day, categories as
non-overlapping covers whose composition reconciles to the bill. The
seeded demo's yearly totals now match the spreadsheet.

Pages and navigation

The period lives in the URL and every page reads the same contract, so a
link, a reload and the browser's Back button keep it. Shared components
carry it: page header with breadcrumbs, period toolbar, theme-aware chart
with an accessible table beside it, metric cards, comparison and
availability states, attention items that each link to the one action
that fixes them. Meter detail leads with an Analysis tab and resolves its
tabs by key; the energy page has Overview, History, Flow and Meters; the
old cost-only Trends page is a general Analysis page over portfolio, type,
category, meter or a meter comparison. Records tabs are paged server-side
instead of showing the latest 200. Everything is English and German,
light and dark, down to 360px.

Some figures change on purpose; docs/RELEASE_NOTES.md lists each one and
what the first start after the update does (it rebuilds all analysis data
before the web server listens). docs/SDD.md and CLAUDE.md describe the
system as it now is.

Tests: 1,733 Core and 746 integration, all green, plus an opt-in
performance suite with a synthetic 1,000-meter generator.
This commit is contained in:
Florian Schmidt
2026-09-20 10:29:13 +02:00
parent c0f52dbb6f
commit 8940ef25c3
384 changed files with 82753 additions and 4518 deletions
+372
View File
@@ -0,0 +1,372 @@
# Dashboard, navigation, and historical analysis: change brief for Claude Code
**Status:** proposed implementation brief; no application changes made by this review.
**Reviewed:** 2026-09-19, repository revision `c0f52db`.
**Scope:** overview dashboard, navigation, meter detail, energy-type pages, trends, virtual meters, and the relevant calculation services.
## 1. Objective and review boundaries
Make MeterVault feel like one coherent application in which users can find an option where they expect it, inspect historical data at any useful period, and understand why a particular number is unavailable.
The central requirement is that a virtual meter combining two meters must have the same applicable consumption/generation analysis as a physical meter: period totals, history, comparisons, quality information, and costs where a valid costing rule exists. Lack of raw readings is expected for a virtual meter and must not prevent derived analysis.
This is a source-code and product-flow review, not a browser usability test. Findings below are grounded in the checked-in Razor components, services, models, migrations, and tests. No running instance, production data, screenshots, or query timings were inspected. Layout improvements are implementation proposals; verify them in a running seeded instance before declaring completion.
Read [CLAUDE.md](../CLAUDE.md) and [SDD.md](SDD.md), especially §§5.45.5, 7.47.5, 8, 10, and 14.1. The historical analysis work also fills existing SDD §8.3 requirements. Treat the phases below as incremental work on the existing application, not a restart of M0M7. Proposed product defaults in this brief are explicit design decisions for this work, not claims about existing behavior. Document any necessary deviation from the SDD, including any deliberately changed reconciliation result.
## 2. Confirmed problems and their causes
Paths below are relative to the repository root. Method/component names are provided because line numbers will move during implementation.
| ID / priority | Finding and user impact | Evidence / implementation starting point |
|---|---|---|
| A01 / P0 | Virtual meters are explicitly excluded from meter period analysis. A functioning combined meter is redirected to the flow page instead of getting its own history. | `src/Infrastructure/Dashboard/MeterPeriodService.cs`, `GetAsync`: returns `null` for `MeterMode.Virtual`. `src/App/Components/Pages/MeterDetail.razor`: virtual notice; `tests/Integration.Tests/MeterPeriodServiceTests.cs`, `A_virtual_meter_reports_nothing_rather_than_a_confident_zero` pins this limitation. |
| A02 / P0 | Virtual means different things in different layers. The flow service sums all upstream meters for every virtual meter; the core normalizer supports expressions; the editor exposes upstream selection without a formula editor. A subtraction formula can therefore disagree with flow. | `FlowService.GetFlowAsync`, `Core/Normalization/Normalizers/VirtualNormalizer.cs`, `Infrastructure/Normalization/MeterConfigFactory.ParseVirtual`, `Shared/MeterEditor.razor`. |
| A03 / P0 | The documented virtual read/materialization pipeline is incomplete in the inspected services. Normalization skips virtual meters, and costing reads stored consumption without resolving virtual expressions. Assigning a cost category is not a demonstrated fix for missing virtual analysis. | `Infrastructure/Normalization/NormalizationService.cs`, `RecomputeMeterAsync`; `Costing/CostService.cs`, `GetMeterCostsAsync` and `QueryConsumptionAsync`. Verify all write paths before introducing any materialization. |
| A04 / P0 | Costs can count overlapping meters more than once. Overview totals sum every meter; energy-type cost sums every meter of the type, whereas its throughput is a topology-root total. Categories deduplicate meter IDs only within that category, not parent/child coverage. | `DashboardService.ActiveMeterIdsAsync` / `TotalCostAsync`; `Pages/EnergyView.razor`, `LoadAsync`; `CostService.GetCategoryCostsAsync`. This is a structural risk; actual inflation depends on topology, memberships, and tariffs. |
| A05 / P0 | Zero, missing data, missing prices, and invalid calculations are conflated. History fills absent months with zero and hides all-zero history. Missing tariffs resolve to zero. Virtual evaluation substitutes zero for absent source timestamps and non-finite results. | `MeterPeriodService.BuildHistory`; `Core/Costing/TariffResolver.ResolveValue`; `VirtualNormalizer.Normalize`. `EnergyView.NodeValue` also falls back to zero. |
| A06 / P1 | Time ranges are inconsistent. Overview has no selector; meter detail has a fixed 12-month mini-chart; Trends defaults to 24 months with Apply; energy/Solar/consumables default to 60 months and reload immediately. “All time” is 1,200 months. | `Pages/Dashboard.razor`, `MeterDetail.razor`, `Trends.razor`, `EnergyView.razor`, `Solar.razor`, `Consumables.razor`. |
| A07 / P0 | Comparison and cutoff semantics differ. Dashboard summary requests full calendar years; breakdown ends at `asOf.AddMonths(1)`; difference truncates that span to whole months. Several pages derive today from UTC. Meter quantity SQL has no upper bound and starts at UTC Jan 1, while its cost query stops at now. | `DashboardService.GetSummaryAsync` / `GetCategoryDifferenceAsync`; `Dashboard.OnInitializedAsync`; `MeterPeriodService.MonthlySql` / `GetAsync` / `LoadCostsAsync`. Future-dated rows and local year boundaries can produce inconsistent totals. |
| A08 / P1 | History is too limited to investigate changes. Trends is total monthly cost only. Energy pages provide a flow diagram and meter list, without a historical series. Meter consumption/readings tabs show only the latest 200 records. | `Pages/Trends.razor`, `EnergyView.razor`; `MeterDetailService.MaxRows`; SDD §8.3 calls for more. |
| A09 / P1 | Overview and Trends can disagree even for matching dates: summary includes manual costs, monthly trend does not. “Latest month with data” supplies an amount without its month and derives recency only from consumption. | `DashboardService.TotalCostAsync`, `GetMonthlyTrendAsync`, `LatestMonthCostAsync`; `DashboardSummary`. A manual-cost-only instance is not handled consistently. |
| A10 / P1 | Navigation mixes analysis by energy type with specialized Solar/consumables pages. Type links open a page headed “flow,” not a general type overview. Important settings remain spread between the meter editor, Sources tab, and admin pages. Existing shortcuts help but do not provide a consistent analysis journey. | `Layout/NavMenu.razor`, `Pages/EnergyView.razor`, `MeterDetail.razor`, `Shared/MeterEditor.razor`, `MeterLinks.cs`. |
| A11 / P1 | Visual semantics vary. Meter history is a custom 110px HTML bar chart; other charts use ApexCharts. Its bars use absolute values, obscuring negative results. Chart components hardcode dark mode despite the app theme toggle. | `MeterDetail.BarStyle`; `Shared/SeriesChart.razor`, `TrendChart.razor`, `CategoryDonut.razor`; `Layout/MainLayout.razor`. |
| A12 / P1 | Labels and formatting can mislead: most cost views call `Format.Euro`, meter detail uses configured currency; `Meter.Unit` is the raw unit and is also used for normalized period results. This needs explicit handling for runtime conversions. | `App/Format.cs`; `MeterPeriodView.Unit`; `Core/Domain/Meter.cs`; `RuntimeCounterNormalizer`. |
| A13 / P1 | Rapid navigation/filter changes can be discarded by an `_loading` early return. Errors generally lack a panel-level retry state; nav DB errors silently remove the energy-type links. | `EnergyView.LoadAsync`, `Solar.LoadAsync`, `Trends.LoadAsync`, `NavMenu.LoadEnergyTypesAsync`. Reproduce stale-page behavior with delayed requests. |
| A14 / P1 | Long-history reads are aggregated directly from `consumption` in several services. Existing continuous aggregates are not a drop-in solution: the migration fixes the zone to Berlin and stores only amount sums, without coverage/quality. Flow sums all meters before filtering to the selected type. | `CostService.QueryConsumptionAsync`, `MeterPeriodService.MonthlySql`, `FlowService.GetFlowAsync`, `Persistence/Migrations/20260713094634_ContinuousAggregates.cs`. Actual performance and refresh behavior require measurement. |
P0 means calculation/meaning must be settled before exposing more totals. P1 is required for the finished user experience, not optional polish.
## 3. Target navigation and user journeys
### 3.1 Sidebar and terminology
Use one stable navigation structure:
```text
Overview /
Analysis /trends (retain route; improve existing page)
Meters /meters
Energy types expandable group of user-defined types
<type name> /energy/{id}
Specialized views
Solar /solar
Tanks & consumables /consumables
Data import /import
Configuration existing /admin/* routes
Energy types / Tariffs / Cost categories / Connectors / Settings
```
- “Energy types” is the analysis entry; “Configuration → Energy types” edits definitions. Make that distinction visible in page titles and descriptions.
- Keep specialized views grouped and available with useful setup states. Do not infer electricity or oil from names or IDs; scope relevant links by capabilities and configured roles.
- Preserve existing routes and bookmarked query parameters. Extend the current helpers rather than constructing competing URLs in individual components.
- Add breadcrumbs: `Overview → <energy type> → <meter>`. Preserve the incoming analysis period and selected metric through drill-down and Back navigation.
- Persist expanded navigation groups and ensure the active item is visible after reload. If type links fail to load, keep the group with an error/retry affordance instead of silently removing it.
- Keep global meter search and its quick-entry actions. Show search text on desktop and an accessible icon on narrow screens. Search results should link to analysis and appropriate entry actions without forcing a trip through the meter list.
### 3.2 Concrete discoverability requirements
| User intention | Required path |
|---|---|
| Understand this period's usage/cost | Overview → energy-type card or cost breakdown row → scoped Analysis |
| Explain a spike | Chart bucket → finer supported period / table → meter → relevant records/events |
| Inspect a combined meter | Meter search/list/type list → virtual meter → Analysis, with source contributions visible |
| Compare two historical years | Analysis → select scope and years → previous-year overlay and comparison table |
| Add a reading or delivery | Existing meter header and list quick action; no need to locate a tab first |
| Change the source/connector | Meter → Sources → Edit connection; preserve the existing connector detour and draft |
| Understand a missing cost | Cost panel explanation → tariff editor scoped to the relevant meter/type and dates |
| Configure a virtual sum | Add/edit meter → Virtual → Sum → select source meters by name → preview |
| Configure energy topology | Energy type → Flow → Manage connections, with clear source/destination names |
Keep the existing successful behaviors: shared meter editor, event rules, stable manual-entry keypad, tank setup shortcut, source draft restoration, one-shot URL actions, and nav refresh after energy-type edits.
## 4. Shared period and analysis contract
Create a reusable analysis query/result contract and period selector. Suggested names are illustrative; fit the existing project conventions.
### 4.1 Query state
An `AnalysisQuery` should carry scope (meter/type/category/explicit meter selection/overview), metric, local start date, local end date, bucket, comparison, and aggregation basis. Resolve relative presets against an injected `TimeProvider` and the configured instance timezone.
- Presets: this month to date, last complete month, year to date, previous calendar year, last 12 months, last 24 months, all available history, custom dates.
- Default Overview to month to date. Default history pages to last 12 months including the current partial month. This means exactly 12 calendar buckets, not 13 or a partial extra future month.
- Display the effective dates next to the preset, plus the timezone in the range details.
- The UI's inclusive end date becomes a local-midnight exclusive upper bound on the next day. To-date presets stop at the captured current instant. Use the same resolved bounds for quantities, costs, comparisons, and exports.
- Never silently include future-dated readings in a “to date” total. A deliberately selected future range should distinguish recorded future data from actual-to-date and projections.
- All available history comes from availability metadata, not an arbitrary century-long range.
- Presets apply immediately. Custom date editing applies once both dates form a valid range, using one consistent Apply interaction across pages.
- Encode state in query parameters, e.g. `/meters/42?tab=analysis&from=2025-01-01&to=2025-12-31&bucket=month&metric=generation&compare=previous-year`. Use stable invariant tokens and localized visible labels.
- Use the URL as the authoritative state for reload/share/back. Preserve one-shot `action` handling separately; changing filters must not reopen a reading or source dialog.
- Validate bounds, IDs, enum tokens, maximum series count, and bucket/point limits. Invalid input must produce a recoverable message or documented fallback.
### 4.2 Buckets and comparisons
- Support day, week, month, year, and Auto where the stored data supports them. Week starts Monday in the instance timezone; preserve actual start/end dates for partial weeks.
- Auto chooses an appropriate bucket with at most 400 visible points per series. Explicit choices that exceed the limit should offer a coarser bucket rather than silently truncate.
- Compare complete periods with complete periods. For MTD/YTD, default to the same elapsed calendar portion of the comparison period, including the local time-of-day cutoff. Clamp missing dates at shorter month/leap-year boundaries and show both exact ranges.
- Distinguish actual change from projection. A “vs last year” label must not secretly compare a current-year projection with a prior-year actual.
- Show absolute difference even when percentage is unavailable. A zero or negative baseline yields “percentage not applicable” by default; do not report 0% when a denominator is absent.
- Keep signed values signed. Generation increases and consumption increases do not share the same good/bad interpretation; use metric-specific or neutral colors and explicit wording.
- Projections remain secondary, explicitly labeled, and describe their method. Suppress projections for unavailable, stale, or insufficient coverage; do not extrapolate a lone old monthly reading as though it were live data.
### 4.3 Result and missing-data semantics
Return structured results rather than `null`, `[]`, or `0` with no explanation. Each series needs stable meter/scope identity, quantity kind, normalized unit/currency, available range, effective requested range, calculation basis, and bucket-level values/status.
Keep separate dimensions: availability (available/missing/partial/error), provenance (measured/manual/estimated/interpolated/derived), freshness, and price coverage. A derived value can be complete and current; these are not mutually exclusive states.
| Situation | Display and action |
|---|---|
| Valid observations yield zero | Show numeric zero, an actual chart point, and its coverage |
| No values in selected range, older history exists | “No data for this period”; show available dates and “Go to latest data” |
| No normalized history yet | Explain the mode-specific next step; do not assume every counter requires two readings because initial-baseline behavior already exists |
| Partial source coverage | Show a partial total only if meaningful, identify missing periods/sources, exclude it from confident comparisons |
| Valid quantities, no applicable tariff | Keep quantity analysis; show cost as unavailable with a tariff action |
| Explicit applicable zero-priced tariff | Show a valid zero cost |
| Invalid virtual expression / missing dependency | Name the problem and affected source; offer Edit calculation or Open source |
| Query/refresh error | Local panel error with Retry; distinguish retained stale data from current data |
| Valid virtual meter without raw readings | Show derived analysis; raw-reading controls are not applicable |
Do not promise an exact coverage percentage unless the stored metadata supports it. Monthly observations are not evidence of day-level completeness. Expose source resolution and known coverage bounds; where unknown, say so. Totals, charts, tables, comparisons, and exports must share these semantics.
## 5. Virtual meters as full analysis subjects
### 5.1 Canonical definition and editor
Separate **calculation dependencies** from **physical flow topology**. An upstream link says where energy flows; it must not silently overwrite a configured formula.
- Provide Sum, Difference, and Advanced expression modes in the shared editor, using source meter selectors with names, quantity kinds, and compatible units.
- Store one canonical definition: expression, referenced IDs, result kind, result unit, and supported evaluation/aggregation semantics. Derive referenced IDs from validated expressions or verify they agree exactly.
- Sum of two generation meters defaults to generation. Consumption sums default to consumption. Mixed-kind/net calculations require an explicit result meaning. Do not retain the current unconditional consumption kind.
- Validate self-reference, cycles including nested virtual meters, missing IDs, syntax, unit compatibility, and result semantics on save and again on read for legacy data. Reuse the restricted expression evaluator; do not evaluate arbitrary code.
- Give the editor a preview for the selected historical period, including per-source values and incomplete-data warnings. Show friendly source names beside any `m123` formula tokens.
- Show the formula and linked dependencies on virtual meter detail. Replace register/baseline/source-ingestion controls with appropriate calculation controls; keep any applicable note/event capability.
### 5.2 Existing data compatibility
The seeded `Summe Solar` meter is virtual with upstream links but no explicit formula (`Infrastructure/Import/ReferenceDataImporter.cs`). Do not break this example or existing installations configured the same way.
1. Preserve existing explicit expressions as authoritative.
2. For expression-less virtual meters with upstream links, compatible normalized units, and unambiguous quantity kind, migrate the existing implied sum to an explicit dependency definition. Update seed creation too.
3. Preserve the topology links as topology; changes to flow links after migration must not secretly alter a saved calculation. Offer an explicit calculation edit when desired.
4. Flag ambiguous/mixed-unit/cyclic/no-source definitions as needing configuration. Do not invent conversions or overwrite metadata unrelated to virtual calculations.
5. Make migration idempotent and report converted/unresolved meter counts. Never modify raw readings. Describe any historical semantic change in release notes.
### 5.3 Evaluation rules
Introduce a shared Infrastructure reader (for example `MeterSeriesService`) used by meter history, energy analysis, dashboard, costing, and flow value lookup. Physical meters read normalized aggregate data; virtual meters recursively resolve dependency series.
- Load all unique physical dependencies in bounded batches and evaluate the dependency graph in topological order. Detect cycles and enforce depth/series/point limits.
- Align source buckets by canonical instants and timezone, not exact raw timestamps or localized chart labels.
- Missing source data is unknown, not zero. Under the default strict policy, a sum bucket is complete only when all required inputs are available for that bucket. Explicitly known zero is a valid input. Preserve provenance from dependencies.
- Propagate nested failures with a useful dependency path. Non-finite arithmetic, including division by zero, produces an invalid bucket with an explanation, never a fabricated zero.
- Additive formulas such as `m1 + m2` and `m1 - m2` can roll up their evaluated base buckets. Arbitrary expressions are not necessarily additive: `sum(m1 / m2)` is not `sum(m1) / sum(m2)`.
- Define an evaluation basis for non-additive expressions and a metric-appropriate reducer (e.g. ratio of totals or weighted average). If those semantics are not supported, make that metric/granularity explicitly unavailable; do not silently change the formula's meaning when zooming.
- Preserve negative net values in history. Sankey rendering may use a separate nonnegative/directional representation, but its rendering limitation must not alter the canonical analysis value.
- Evaluation must not depend on cost-category membership. Implement read evaluation first. If costing needs materialized results under SDD §14.1, use the same evaluator with explicit dependency invalidation, rebuild rules, and tests.
- Invalidate caches after source ingestion, corrections, events, import/revert, normalization changes, definition edits, and relevant tariff edits. Avoid process-wide unbounded caches or recomputation per chart cell.
### 5.4 Minimum worked example
Given generation meters A and B with complete monthly data:
| Month | A | B | Virtual Sum A+B |
|---|---:|---:|---:|
| January | 100 kWh | 150 kWh | 250 kWh |
| February | 80 kWh | 120 kWh | 200 kWh |
The virtual meter shows 450 kWh for the two-month period, a generation label, both history points, source contribution details, and the same numbers in type analysis. It needs no raw readings and no cost category. If B is missing in February, that month is incomplete, not a confident 80 kWh. If B has an observed zero, the complete result is 80 kWh. A separate AB meter shows 50 and 40 kWh rather than inheriting the flow service's sum.
## 6. Totals, costs, and energy-type semantics
Do not achieve visual consistency by making every page sum all meters. Define a shared aggregation policy and include its selected basis in results and visible explanations.
### 6.1 Quantity totals
- Separate consumption, generation, runtime, tank balance, and net quantities. Same energy-type membership does not guarantee addable units or independent measurement coverage.
- Default physical throughput to the appropriate non-overlapping topology roots, with consumption and generation separate. List exactly which meters contribute and which are excluded.
- A virtual view of already-counted sources is visible and analyzable but excluded from an additive portfolio total by default. Explicit selections can replace source coverage with a virtual result; they must not add both.
- Detect known overlap using topology and virtual dependencies. Do not claim completeness where overlapping measurements cannot be established from configuration; request scope configuration through a clear page action.
- Distinguish throughput from billed import and total household use. Multi-parent topology and grid-plus-solar supply do not justify summing every node as “consumption.”
- Retired meters retain their historical contribution. `IsActive` controls current operation, not erasure from historical totals; respect effective install/retire dates where reliable.
### 6.2 Costs
- Make cost inclusion explicit at the relevant meter/scope configuration. Default new virtual meters to analysis-only for portfolio costing so enabling their analysis does not increase the bill.
- Meter detail may show the cost of a physical or virtual scope without automatically including that scope in portfolio totals.
- Virtual costing must name its rule: tariff applied to the virtual quantity, or aggregation of already-priced independent sources. These differ when sources have different tariffs. Default to unavailable until a valid rule is inferable or configured; never sum source costs and reprice the combined quantity together.
- Handle standing charges once for the intended billing scope. Do not replicate a type/global base charge across every analytical submeter and virtual view. Preserve existing tariff precedence and document any changed billing rule.
- Add applicable price coverage to cost results. Historical tariff gaps produce partial/unavailable cost; existence of any tariff anywhere is insufficient. Missing optional credits must be distinguishable from missing required unit prices.
- For a year with tariff changes, aggregate correctly priced underlying billing periods; do not price the whole year from a July sample. Changing chart granularity must not change the total bill. Keep the existing monthly pricing convention unless a deliberate change is documented and reconciled.
- Include manual costs exactly once in matching overview, trend, and category totals. Show uncategorized contributions rather than dropping them. If categories overlap, label them as overlapping views and do not present their sum/donut as a disjoint breakdown of the bill.
- Use signed bars/tables for cost credits and negative totals. A donut is appropriate only for a nonnegative, disjoint composition.
- Make “Latest month with data” return its actual period and availability basis, including manual costs. Never silently switch every dashboard panel to historical data; offer an explicit action to open that period.
- Use configured currency consistently. Use normalized quantity units for analysis and raw units only for raw register values; resolve runtime-to-volume conversion explicitly.
## 7. Page specifications
### 7.1 Overview dashboard
Use a consistent header, period toolbar, and compact coverage/freshness summary. The initial viewport should answer: what happened in this period, what changed, and where to investigate.
1. Period cost with coverage and comparison. Show usage/generation per energy type with their own units, rather than adding unlike quantities to one total.
2. Energy-type cards with quantity, available cost, comparison, and a clear link to that type's analysis. Quantity cards work even without tariffs or categories.
3. Shared historical chart with metric toggle and previous-period overlay; the selected range applies to every analytical panel.
4. Ranked change table by category or meter: current, previous, absolute delta, percentage where meaningful. Rows link to scoped analysis with the same dates.
5. Cost composition that reconciles to the selected scope, with explicit overlapping/uncategorized handling.
6. Compact attention items only for relevant issues: missing prices, missing data, invalid virtual dependencies, stale sources. Provide a targeted action for each.
Keep the update banner separate from analytical status. Avoid making configuration of cost categories a prerequisite for viewing valid quantities.
### 7.2 Meter detail
Move the tab/navigation bar directly below the identity and action header. Put the analytical content inside the default **Analysis** tab so Sources and Events do not sit below a long wall of charts.
- Tabs: Analysis, Readings where applicable, Normalized data, Events, Tariffs, Sources for physical input meters / Calculation for virtual meters.
- Preserve old `tab=readings|consumption|events|tariffs|sources` links with a compatibility mapping. Resolve tabs by stable keys and capability, not fixed numeric indexes after conditional tabs are introduced.
- Analysis: shared period controls; selected-period quantity/cost/comparison; actual-versus-projection distinction; full-size chart; year-over-year view; accessible table; CSV export; data-quality/coverage explanation.
- History table: period, quantity, cost, comparison, quality, and coverage. Include year in date labels across multi-year ranges. Chart selection can drill to supported finer detail while retaining scope.
- Show lifecycle events and tariff changes as optional contextual markers, bounded to the selected range. A chart click should lead to records/events capable of explaining that interval.
- Raw and normalized-data tabs need server-side date filtering and pagination with a stable ordering. The latest-200 view is not full history. Explain raw retention separately from retained analytical history.
- Virtual Analysis includes source contributions and formula details. Do not show fake register totals or suggest adding a raw reading to fix virtual history.
### 7.3 Energy-type page
Title the page with the energy type's user-defined name. Use **Overview / History / Flow / Meters** tabs, defaulting to Overview.
- Overview: separate appropriate quantity kinds, cost with billing basis, coverage, trends, and largest changes.
- History: shared chart/table, day/week/month/year selection, calendar-year comparison, and optional per-meter series. Offer “total” and “individual meters” views with overlap explanations.
- Flow: retain Sankey as a topology tool, using the canonical meter values. Mark inferred proportional allocations as estimates. Provide a textual/table equivalent and a connection-management entry point.
- No topology must not prevent analysis. Negative/net values remain available in History even if unsuitable for a ribbon.
- Meters: searchable list with period values, quality/coverage, physical/virtual distinction, and existing quick actions. A missing graph node must not become a fake zero meter value.
- Add appropriate links to Solar/consumables without making users rediscover a different period selector there.
### 7.4 Analysis page (existing `/trends`)
Replace the single monthly total-cost chart with one reusable exploration page. Scope selector: portfolio cost, energy type, category, individual meter, or explicit meter comparison. Metric selector: supported quantity kind or cost. Support at most six simultaneous meter series by default, with an explanation when the selection exceeds that limit.
Use the same series reader, toolbar, chart, and table as meter/type pages. Do not build a second formula engine here. Comparable meter quantities must have compatible normalized units; otherwise split charts or explain why the comparison is unavailable. Category analysis is always available for cost; quantity analysis needs a single compatible quantity kind/unit.
### 7.5 Solar and consumables
Adopt the shared toolbar, theme, cards, history components, and missing-data semantics while retaining their specialized measures. Distinguish current tank balance/forecast from historical period totals. Show the balance's observation date explicitly; a historical date range must not label today's balance as a historical observation.
Replace visible instructions to edit raw role tags with friendly meter-role configuration controls. Missing Solar context should identify the required role and provide a scoped setup path. Use configured units/conversions rather than assuming every generation counter is measured in kWh.
## 8. Shared UI and interaction standards
- Extract common components for page header, period toolbar, metric card, analysis chart/table, and availability state. Reuse MudBlazor and the existing ApexCharts integration.
- Use the existing theme for colors, spacing, borders, typography, and density. Charts must respond to light/dark changes in the current circuit.
- Chart points retain temporal identity, nullable value, and provenance; avoid reducing them to only `string Label, double Value` before rendering. Preserve numerical precision in calculation and round only for display.
- Display units in axes/tooltips and currency in cost series. Do not smooth or connect across unknown intervals. Render signed bars around a real zero baseline.
- Provide keyboard-accessible links/actions, chart table alternatives, touch-friendly controls, visible focus, and meanings that do not rely only on red/green.
- Use localized EN/DE strings in both resources and existing typed accessors. Format user data without translating meter/type names. Cover long German labels on narrow screens.
- Validate at approximately 360px, 768px, and desktop widths. Controls should wrap predictably; keep page-wide overflow out of the main layout and contain wide tables locally.
- Cancel superseded loads or use request-generation IDs so only the latest requested scope/range is committed. Publish a coherent result atomically; do not mix the previous type's chart with the next type's title.
- Expose initial loading, refresh, error/retry, and stale-but-visible states consistently. Dispose subscriptions/cancellation sources with the circuit/component.
## 9. Data access, history resolution, and performance
Preserve immutable raw readings and existing normalization rules, including month-label attribution, local-month splitting, swaps/resets, and import/revert behavior. Do not rescan raw readings to draw long-range charts.
1. Centralize physical/virtual analytical reads in Infrastructure. Keep the formula algebra, compatibility checks, and reducers pure in Core where practical; keep presentation out of the domain model.
2. Select meter IDs and time bounds in SQL before aggregation. Batch dependency and tariff reads. Avoid one full scan/context/tariff load per meter per panel.
3. Audit existing continuous aggregates before switching readers: timezone, real-time/materialized behavior, backfill after historical imports, late corrections, retention, and current incomplete buckets. Their comments are not proof of freshness.
4. Add migrations for required aggregate/metadata changes rather than editing previously applied migrations. Support the configured instance timezone, not a hardcoded Berlin-only implementation.
5. Preserve coverage/provenance and available resolution through aggregation. The existing amount-only aggregates cannot answer these questions by themselves; define auxiliary summaries or sufficient aggregate fields.
6. Legacy monthly data must remain honestly monthly. Daily/week views cannot display one month-end amount as a measured daily spike. If interpolation is offered, make it explicit, mark it interpolated, and preserve monthly totals; otherwise explain the unavailable resolution.
7. Never include both a refreshed aggregate bucket and its underlying consumption in a live-tail merge. Test exact refresh boundaries and backfilled periods.
8. Bound raw/event retrieval and chart output separately. Prove history still works after raw readings are removed by the retention policy.
9. Keep current public API contracts compatible. Reuse the shared reader behind relevant endpoints where possible; add versioned/additive fields for availability rather than silently changing existing numeric response types.
10. Record representative timings and query plans before/after. Proposed review target: cached metadata plus a 10-year monthly request for 100 selected meters should complete within two seconds on documented local test hardware. Treat this as a target to measure, not a verified property. Test 1,000-meter selection/query planning and enforce output limits without a per-meter scan storm.
## 10. Implementation phases and exit criteria
### Phase 1 — Shared semantics and regression fixtures
- Add frozen-clock period resolution, analysis contracts, zero/missing/partial semantics, normalized units, and explicit aggregation/cost policy.
- Add fixtures reproducing virtual absence, overlapping scopes, missing prices, historical-only data, and inconsistent cutoffs.
- Resolve proposal choices in this brief in a short implementation note; identify any incompatible legacy assumptions before migration.
**Exit:** deterministic tests pin interval boundaries, scope membership, missing-data rules, and arithmetic semantics. No new UI claims rely on unresolved totals.
### Phase 2 — Virtual evaluation and compatible migration
- Implement shared physical/virtual series reader, canonical definitions, validation, editor, compatibility migration, and source contribution results.
- Remove the blanket virtual rejection from period analysis. Replace the old “virtual returns null” test with positive and error-state behavior tests.
- Route flow and cost quantity lookup through the same evaluator, keeping flow rendering separate.
**Exit:** the worked A+B/AB examples and seeded Summe Solar have full analysis without raw readings/category setup; nested/error cases are explained; enabling analysis does not double portfolio totals.
### Phase 3 — History and page navigation
- Implement shared toolbar/chart/table; upgrade meter, energy-type, and Analysis pages.
- Add period-preserving URLs, breadcrumbs, compatibility tab routing, history pagination, exports, and stale-request protection.
**Exit:** a user can navigate Overview/type → meter → historical month and back without losing dates, and compare historical years for physical and virtual meters.
### Phase 4 — Overview and specialized consistency
- Rework dashboard quantities/costs/change tables, aligned manual costs, latest-data period, targeted setup actions, sidebar grouping, and Solar/consumable controls.
- Complete theme, currency, normalized-unit, localization, mobile, and accessibility work.
**Exit:** matching scopes/periods reconcile across cards, charts, tables, and exports. Valid quantity analysis remains visible when costs are unavailable.
### Phase 5 — Integration, performance, and documentation
- Run targeted regression suites and then the required full build/tests. Measure query behavior with historical and synthetic data.
- Walk through the seeded application in EN/DE, light/dark, and mobile/desktop. Capture representative before/after screenshots and any remaining limitations.
- Update SDD/CLAUDE descriptions that currently claim unsupported virtual behavior or no longer describe the navigation/calculation model.
**Exit:** all acceptance scenarios below pass or have an explicitly documented blocker. Do not mark the feature complete after only changing menus or removing the virtual `return null`.
## 11. Acceptance scenarios and validation
| Scenario | Required evidence |
|---|---|
| Two-source virtual generation sum | 100+150=250 and 80+120=200 by month; 450 total; generation units; physical and virtual pages/type history agree |
| Missing versus zero source | Missing B makes the bucket partial/unavailable; observed B=0 yields a complete sum |
| Difference and nesting | Negative AB plots below zero; nested dependencies resolve once; cycle reports a named dependency error |
| Invalid arithmetic | Division by zero and unsupported non-additive rollup produce explanations, never zero or infinity |
| Legacy virtual configuration | Seeded Summe Solar acquires a compatible definition; migration rerun changes nothing; existing formulas retain precedence |
| Overlapping topology | Parent 300 and child 100 display 300 for the non-overlapping parent scope, with 100 as a breakdown, not 400 |
| Virtual overlap | Sources 100+150 and virtual 250 remain a 250 portfolio quantity where that is the selected coverage, not 500; costing has equivalent explicit coverage |
| Missing versus free tariff | Valid quantity with missing required price has unavailable cost; explicit zero tariff has valid zero cost |
| Cost stability | Manual costs appear once; standing charges follow the chosen billing scope; changing chart bucket does not reprice annual totals from one sampled tariff |
| Historical-only instance | Current period explains no data and offers actual available dates; selecting historical year shows quantities/costs for physical and virtual meters |
| Manual-cost-only instance | Overview, trend, category breakdown, and latest-data month agree without requiring a meter |
| Local-time boundaries | Frozen clock tests at local New Year, Berlin spring/fall DST, leap day, shorter months, and a zone behind UTC; from/to are half-open consistently |
| Future rows and partial periods | To-date totals exclude future data; comparisons state actual matched dates; projections are explicitly separate |
| Zero/negative history | Legitimate all-zero year remains visible; negative net history is signed and percentage rules are consistent |
| Monthly legacy resolution | Monthly import stays monthly or explicitly interpolated; daily chart never invents measured detail |
| Retained history | Charts still work after old raw readings are absent; raw-record tab explains retention |
| Rapid filter/navigation change | Delayed first request cannot overwrite the later selected type/range; reload/back preserves URL state |
| Compatibility actions | Existing reading/event/edit/source URLs still open the intended action once; connector detour preserves typed draft and return context |
| UI consistency | Theme toggle updates charts; EN/DE resource tests pass; narrow-screen controls and keyboard/table alternatives work |
| Import/correction freshness | Import, revert, reading deletion, swap/reset, and tariff/definition edits refresh affected physical/virtual history without duplicates |
Use the existing suites as starting points:
- `tests/Core.Tests/VirtualMeterTests.cs`, `ExpressionEvaluatorTests.cs`, and relevant normalization tests.
- `tests/Integration.Tests/MeterPeriodServiceTests.cs`, `FlowServiceTests.cs`, `CostSetupTests.cs`, and costing/reconciliation suites.
- `tests/Integration.Tests/DashboardRenderTests.cs`, `LocalTimeEntryTests.cs`, and `Localization/StringResourceTests.cs`.
Add tests around substantive data behavior and navigation state; do not rely on snapshots of markup alone. Server-render tests do not prove interactive ApexCharts updates, browser history, or responsive usability. Add meaningful interaction coverage using the repository's available tooling, or record a manual browser checklist when no suitable harness exists.
Commands for the implementing agent:
```powershell
dotnet build
dotnet test tests/Core.Tests
dotnet test tests/Integration.Tests
```
Integration tests need Docker/TimescaleDB. Report actual commands/results and prerequisites that prevented execution. Preserve golden CSV reconciliation unless a deliberate correctness fix is described with old/new values and a focused regression test. The source review that produced this brief did not run these tests.
## 12. Delivery requirements for Claude Code
Deliver working code in reviewable phases, necessary migrations, both language resources, substantive tests, and updated documentation. Keep the current stack and user-defined energy types. Do not rewrite ingestion, introduce a new frontend, or change raw-reading history to make charts look correct.
The final implementation report should state which findings were fixed, how physical and virtual results now agree, which cost/aggregation decisions were applied to existing data, what was tested, and any remaining limitations. Include screenshots of the new Overview, physical/virtual meter history, and energy-type history at desktop and mobile widths.
**Definition of done:** a user can find the correct action without guessing which page owns it, examine the same selected period consistently across the application, analyze a valid combined virtual meter as fully as its compatible physical inputs, and distinguish a true zero from unavailable or incomplete information.