Audit fixes: batch recompute, negative-baseline percentages, key-ring persistence
ci / build-test (push) Successful in 1m17s
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:
@@ -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>
|
||||
|
||||
@@ -63,8 +63,14 @@ public sealed class IngestionService(
|
||||
}
|
||||
|
||||
/// <summary>Ingests directly against a meter (REST push, e.g. Home Assistant POST /api/v1/readings).</summary>
|
||||
/// <param name="renormalize">
|
||||
/// False to skip deriving consumption, for callers ingesting a batch into one meter: recomputing
|
||||
/// rewrites the meter's entire series, so doing it per reading is quadratic in batch size. Such a
|
||||
/// caller must recompute the affected meters itself once the batch is in.
|
||||
/// </param>
|
||||
public async Task<IngestionOutcome> IngestByMeterAsync(
|
||||
int meterId, DateTimeOffset time, double value, CancellationToken cancellationToken = default)
|
||||
int meterId, DateTimeOffset time, double value, bool renormalize = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var meter = await _db.Meters
|
||||
.FirstOrDefaultAsync(m => m.Id == meterId, cancellationToken).ConfigureAwait(false);
|
||||
@@ -82,10 +88,24 @@ public sealed class IngestionService(
|
||||
|
||||
var outcome = await UpsertAsync(meter, utc, value, sourceId: null, cancellationToken).ConfigureAwait(false);
|
||||
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
await RenormalizeAsync(meter.Id, outcome, cancellationToken).ConfigureAwait(false);
|
||||
if (renormalize)
|
||||
{
|
||||
await RenormalizeAsync(meter.Id, outcome, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives consumption for one meter after a batch of readings has been written. The public
|
||||
/// counterpart to skipping <c>renormalize</c> on each individual ingest.
|
||||
/// </summary>
|
||||
public async Task RenormalizeMeterAsync(int meterId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _normalization.RecomputeMeterAsync(meterId, batchId: null, cancellationToken).ConfigureAwait(false);
|
||||
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives consumption from the reading just written. Without this a live-ingested reading sits
|
||||
/// in <c>reading</c> forever and every derived figure — consumption, generation, cost — stays
|
||||
|
||||
Reference in New Issue
Block a user