7e34aeccc8
ci / build-test (push) Successful in 1m24s
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
61 lines
2.3 KiB
C#
61 lines
2.3 KiB
C#
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
|
|
#nullable disable
|
|
|
|
namespace MeterVault.Infrastructure.Persistence.Migrations
|
|
{
|
|
/// <inheritdoc />
|
|
public partial class AddMeterLinks : Migration
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.CreateTable(
|
|
name: "meter_link",
|
|
columns: table => new
|
|
{
|
|
id = table.Column<int>(type: "integer", nullable: false)
|
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
from_meter_id = table.Column<int>(type: "integer", nullable: false),
|
|
to_meter_id = table.Column<int>(type: "integer", nullable: false)
|
|
},
|
|
constraints: table =>
|
|
{
|
|
table.PrimaryKey("pk_meter_link", x => x.id);
|
|
table.CheckConstraint("ck_meter_link_distinct", "from_meter_id <> to_meter_id");
|
|
table.ForeignKey(
|
|
name: "fk_meter_link_meter_from_meter_id",
|
|
column: x => x.from_meter_id,
|
|
principalTable: "meter",
|
|
principalColumn: "id",
|
|
onDelete: ReferentialAction.Cascade);
|
|
table.ForeignKey(
|
|
name: "fk_meter_link_meter_to_meter_id",
|
|
column: x => x.to_meter_id,
|
|
principalTable: "meter",
|
|
principalColumn: "id",
|
|
onDelete: ReferentialAction.Cascade);
|
|
});
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "ix_meter_link_from_meter_id_to_meter_id",
|
|
table: "meter_link",
|
|
columns: new[] { "from_meter_id", "to_meter_id" },
|
|
unique: true);
|
|
|
|
migrationBuilder.CreateIndex(
|
|
name: "ix_meter_link_to_meter_id",
|
|
table: "meter_link",
|
|
column: "to_meter_id");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Down(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.DropTable(
|
|
name: "meter_link");
|
|
}
|
|
}
|
|
}
|