Audit fixes: batch recompute, negative-baseline percentages, key-ring persistence
ci / build-test (push) Successful in 1m17s

Three defects found reviewing the last few commits.

Deriving consumption on ingest made the batch reading endpoint quadratic. A
recompute rewrites a meter's entire consumption series, and POST
/api/v1/readings ran one per reading -- 500 readings for one meter meant 500
full rewrites. IngestByMeterAsync takes renormalize:false and the endpoint
normalizes each touched meter once after the batch.

Percentage change divided by a possibly negative baseline. A net-export meter
going from -100 to -150 exported half again as much and would have been
reported as "+50%", reading as more consumption. A non-positive baseline now
reports no basis rather than a confident lie.

The data-protection key ring had no persistent home outside Docker Compose. The
LXC installer now creates /var/lib/metervault/keys at 0700 -- the app would
otherwise create it under the default umask, leaving a key ring world-readable
-- and the Unraid template maps it, since without that every UI-entered secret
was lost whenever the container was recreated. README documents the variable
and the trust boundary: keys on disk protect against leaked database content,
not against an attacker who already has the host.

Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
This commit is contained in:
2026-07-18 19:39:56 +02:00
parent ad896db051
commit cedd60ab45
8 changed files with 107 additions and 8 deletions
@@ -45,8 +45,13 @@ public sealed record MeterPeriodView(
public bool HasHistory => Last12Months.Count > 0;
/// <summary>
/// Percentage change is only meaningful against a positive baseline. Dividing by a negative one
/// inverts the sign — a net-export meter going from 100 to 150 would report "+50% more used"
/// when it exported half as much again — so those report no basis rather than a confident lie.
/// </summary>
private static double? Ratio(double current, double previous) =>
Math.Abs(previous) < 1e-9 ? null : (current - previous) / previous;
previous <= 1e-9 ? null : (current - previous) / previous;
}
/// <summary>A meter lifecycle/correction event row.</summary>