Skip to content

fix(timeplanning): startup repair for corrupted pauseNId (last 7 days, 5-min sites)#1608

Merged
renemadsen merged 1 commit into
stablefrom
fix/repair-corrupted-pauseid
Jun 16, 2026
Merged

fix(timeplanning): startup repair for corrupted pauseNId (last 7 days, 5-min sites)#1608
renemadsen merged 1 commit into
stablefrom
fix/repair-corrupted-pauseid

Conversation

@renemadsen

Copy link
Copy Markdown
Member

Problem

The mobile punch-clock had been sending the absolute stop-time tick as pauseNId on pause-stop for 5-minute sites instead of the pause duration. The server decodes pauseNId as (pauseNId-1)*5 minutes, so corrupted ids (e.g. 145) became ~12h breaks and drove netto hours negative. Verified on real rows 17003/17024/19545 (ids 116/115/115 → ~9.5h decoded vs ~30min real pause; persisted NettoHours ≈ -0.92h).

The app-side fix (stops new corruption) is in the flutter-time repo. This PR repairs the already-corrupted historical rows.

Fix

New idempotent, corrupted-rows-only startup repair (CorruptedPauseIdRepair), mirroring the existing SeedDatabase startup idiom, scoped to the last 7 days and to 5-minute sites (UseOneMinuteIntervals == false):

  • Oracle = the intact pause timestamps. A slot is corrupt when (pauseNId-1)*5 - actualMinutes > 15 (catches the absolute-tick overstatement; ignores the known 5-min off-by-one).
  • Corrected value (int)(actualMinutes/5)+1, matching the server's /5+1 encode. Writes only on change (idempotent).
  • After correcting a row, re-runs the real production netto writer ComputeTimeTrackingFields so persisted NettoHours/NettoHoursInSeconds are restored (verified this is the flag-off save path's final netto writer).
  • Logs each correction to console and flags corrupt-but-unrepairable rows (no usable timestamps) to console + Sentry for manual review.

No schema change / no migration (pure data update via the DbContext). No base-package edits.

Tests (NUnit + Testcontainers MariaDB)

Control matrix: corrupted-in-window FIXED, correct UNCHANGED, off-by-one LEFT ALONE, out-of-window UNCHANGED, 1-minute-site UNCHANGED; idempotency; netto recompute assertion; and no-write-when-corrupt-id-has-no-timestamps.

🤖 Generated with Claude Code

…, 5-min sites)

Recomputes pauseNId from the intact pause timestamps for rows whose
stored value grossly overstates the real pause (the absolute-stop-tick
corruption), idempotently, on plugin startup. Re-establishes persisted
netto/flex consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 16, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a startup-time, idempotent data repair to correct historically corrupted pauseNId values for 5‑minute sites (last 7 days) by recomputing pause IDs from pause timestamps and re-running the production netto writer so persisted netto fields are restored.

Changes:

  • Introduces CorruptedPauseIdRepair helper that detects and corrects clearly corrupted pause IDs, recomputing persisted netto fields and emitting console/Sentry observability.
  • Hooks the repair into plugin startup right after database seeding.
  • Adds NUnit + Testcontainers MariaDB coverage for in-scope fixes, out-of-scope no-ops, idempotency, and “no timestamps => no write”.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/CorruptedPauseIdRepair.cs Implements the targeted pause-id repair logic and netto recomputation, plus logging/anomaly reporting.
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/EformTimePlanningPlugin.cs Runs the repair at startup after seeding.
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/CorruptedPauseIdRepairTests.cs Adds integration-style tests validating the repair behavior and idempotency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +83
.Where(p => p.WorkflowState != Constants.WorkflowStates.Removed
&& p.Date >= windowStart
&& fiveMinuteSiteIds.Contains(p.SdkSitId))
.ToListAsync()
.ConfigureAwait(false);
Comment on lines +915 to +920
public void RepairCorruptedPauseIds(string connectionString)
{
var contextFactory = new TimePlanningPnContextFactory();
using var dbContext = contextFactory.CreateDbContext([connectionString]);
CorruptedPauseIdRepair.Run(dbContext).GetAwaiter().GetResult();
}
Comment on lines +85 to +89
Assert.That((await Reload(ctx, corrupted)).Pause1Id, Is.EqualTo(7)); // fixed
Assert.That((await Reload(ctx, correct)).Pause1Id, Is.EqualTo(7)); // unchanged
Assert.That((await Reload(ctx, offByOne)).Pause1Id, Is.EqualTo(6)); // left alone
Assert.That((await Reload(ctx, oldRow)).Pause1Id, Is.EqualTo(145)); // out of window
Assert.That((await Reload(ctx, oneMin)).Pause1Id, Is.EqualTo(30)); // 1-min site
@renemadsen renemadsen merged commit 58a2c70 into stable Jun 16, 2026
39 checks passed
@renemadsen renemadsen deleted the fix/repair-corrupted-pauseid branch June 16, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants