mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(migrations): tighten and widen the 0355 repair where the evidence allows
A snooze that had already run out before the send is history, not a choice about the reminded slot, so it no longer keeps a row from being repaired. The "no due date" arm matched nothing, because a reminder cannot be created without a cadence; it now covers a rule whose last occurrence was the one sent, which is open unless it was done or skipped since, while a course whose window closed before the send stays closed through the end-date guard. The start-page step reads a stored item list only when it is a list, so a malformed layout can no longer fail the migration.
This commit is contained in:
1 parent
7aefde67e2
commit
f3df0023b2
2 files changed
+48
-14
No files matched your search
@@ -30,8 +30,9 @@ ALTER TABLE "measurement_reminders" ADD COLUMN "last_notified_at" TIMESTAMP(3);
|
||||
-- longer than seven days, a recurrence rule, or no cadence at all.
|
||||
-- Measurement reminders are included; whether it names a measurement
|
||||
-- type does not matter;
|
||||
-- - it was not done or skipped after that send, and it carries no
|
||||
-- snooze;
|
||||
-- - it was not done or skipped after that send, and no snooze reaches
|
||||
-- past it (a snooze that had already run out before the send is
|
||||
-- history, not a choice about this slot);
|
||||
-- - the send was its most recent claim, and the row's last write
|
||||
-- (`updated_at`) lands within a minute before to ten minutes after
|
||||
-- that claim. The roll-on is the write that follows a delivery by
|
||||
@@ -42,7 +43,9 @@ ALTER TABLE "measurement_reminders" ADD COLUMN "last_notified_at" TIMESTAMP(3);
|
||||
-- and what the new behaviour no longer writes: more than a week past
|
||||
-- the send (a weekly or shorter cycle, including a rule that recurs
|
||||
-- within the week, still rolls on, so it is not repaired), or no due
|
||||
-- date at all for a reminder with no cadence.
|
||||
-- date at all: a rule whose last occurrence was the one sent, which
|
||||
-- is still open unless it was done or skipped since. A course whose
|
||||
-- window ended before the send is excluded by the end-date guard.
|
||||
--
|
||||
-- The due date goes back to the instant of that send, which is the day
|
||||
-- the reminder went out and at its notify hour. `last_notified_at`
|
||||
@@ -77,7 +80,7 @@ WHERE r."id" = lc."reminder_id"
|
||||
OR r."rrule" IS NOT NULL
|
||||
OR (r."interval_days" IS NULL AND r."rrule" IS NULL)
|
||||
)
|
||||
AND r."snoozed_until" IS NULL
|
||||
AND (r."snoozed_until" IS NULL OR r."snoozed_until" <= lc."sent_at")
|
||||
AND r."last_notified_at" IS NULL
|
||||
AND (r."last_satisfied_at" IS NULL OR r."last_satisfied_at" < lc."sent_at")
|
||||
AND (r."last_skipped_at" IS NULL OR r."last_skipped_at" < lc."sent_at")
|
||||
@@ -86,11 +89,7 @@ WHERE r."id" = lc."reminder_id"
|
||||
AND r."updated_at" <= lc."sent_at" + INTERVAL '10 minutes'
|
||||
AND (
|
||||
r."next_due_at" > lc."sent_at" + INTERVAL '7 days 12 hours'
|
||||
OR (
|
||||
r."next_due_at" IS NULL
|
||||
AND r."interval_days" IS NULL
|
||||
AND r."rrule" IS NULL
|
||||
)
|
||||
OR r."next_due_at" IS NULL
|
||||
);
|
||||
|
||||
-- 3. Show booked visits on the start page for people who chose its items
|
||||
@@ -103,13 +102,17 @@ WHERE r."id" = lc."reminder_id"
|
||||
-- non-empty list that lacks it. An empty list is the whole rail turned off
|
||||
-- and stays empty. A person who has deliberately switched visits off since
|
||||
-- v1.37.2 cannot be told apart from one who never had the choice; they see
|
||||
-- visits again and can switch them off once more, which now sticks.
|
||||
-- visits again and can switch them off once more, which now sticks. A
|
||||
-- stored value that is not a list is left alone rather than read.
|
||||
UPDATE "users"
|
||||
SET "dashboard_widgets_json" = jsonb_set(
|
||||
"dashboard_widgets_json",
|
||||
'{enabledHeroItemKinds}',
|
||||
("dashboard_widgets_json" -> 'enabledHeroItemKinds') || '["upcoming_visit"]'::jsonb
|
||||
)
|
||||
WHERE jsonb_typeof("dashboard_widgets_json" -> 'enabledHeroItemKinds') = 'array'
|
||||
AND jsonb_array_length("dashboard_widgets_json" -> 'enabledHeroItemKinds') > 0
|
||||
AND NOT ("dashboard_widgets_json" -> 'enabledHeroItemKinds') ? 'upcoming_visit';
|
||||
WHERE CASE
|
||||
WHEN jsonb_typeof("dashboard_widgets_json" -> 'enabledHeroItemKinds') = 'array'
|
||||
THEN jsonb_array_length("dashboard_widgets_json" -> 'enabledHeroItemKinds') > 0
|
||||
AND NOT ("dashboard_widgets_json" -> 'enabledHeroItemKinds') ? 'upcoming_visit'
|
||||
ELSE FALSE
|
||||
END;
|
||||
@@ -226,7 +226,12 @@ describe("an open fortnightly questionnaire through the reminder tick", () => {
|
||||
source: "MANUAL",
|
||||
},
|
||||
});
|
||||
const next = await tick(new Date(filledAt.getTime() + 15 * 60 * 1000));
|
||||
// Outside the notify hour the open slot is not even polled (the
|
||||
// eventful satisfy worker covers that moment); the next notify hour's
|
||||
// poll resolves it.
|
||||
const afternoon = await tick(new Date(filledAt.getTime() + 15 * 60 * 1000));
|
||||
expect(afternoon.autoResolved).toBe(0);
|
||||
const next = await tick(new Date(DUE.getTime() + 2 * DAY));
|
||||
expect(next.autoResolved).toBe(1);
|
||||
row = await prisma.measurementReminder.findUniqueOrThrow({
|
||||
where: { id: created.id },
|
||||
@@ -338,6 +343,19 @@ describe("migration 0355", () => {
|
||||
updatedAt: new Date("2026-09-26T07:00:03.900Z"),
|
||||
},
|
||||
{ id: "yearly-measurement", data: { measurementType: "WEIGHT" } },
|
||||
{
|
||||
// A snooze that ran out before the send says nothing about this slot.
|
||||
id: "old-snooze",
|
||||
data: { snoozedUntil: new Date("2026-09-20T07:00:00.000Z") },
|
||||
},
|
||||
{
|
||||
// A rule whose last occurrence was the one sent: rolled to nothing.
|
||||
id: "rule-exhausted",
|
||||
data: {
|
||||
rrule: "FREQ=MONTHLY;UNTIL=20260930T000000Z",
|
||||
nextDueAt: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
// An earlier cycle's send is older; the latest send decides.
|
||||
id: "monthly-many-sends",
|
||||
@@ -422,6 +440,15 @@ describe("migration 0355", () => {
|
||||
},
|
||||
{ id: "appointment", data: { origin: "ENCOUNTER", rrule: null } },
|
||||
{ id: "deleted", data: { deletedAt: ROLLED } },
|
||||
{
|
||||
// A course whose window closed before the send stays closed.
|
||||
id: "course-ended",
|
||||
data: {
|
||||
rrule: "FREQ=DAILY",
|
||||
nextDueAt: null,
|
||||
endsOn: new Date("2026-09-25T00:00:00.000Z"),
|
||||
},
|
||||
},
|
||||
{ id: "disabled", data: { enabled: false } },
|
||||
{ id: "never-sent", claims: [] },
|
||||
{
|
||||
@@ -483,6 +510,9 @@ describe("migration 0355", () => {
|
||||
dashboardWidgetsJson: { version: 1, widgets: [] },
|
||||
});
|
||||
await seedUser("hero-none");
|
||||
await seedUser("hero-malformed", {
|
||||
dashboardWidgetsJson: { version: 1, enabledHeroItemKinds: "all" },
|
||||
});
|
||||
|
||||
await runDataSteps();
|
||||
await runDataSteps();
|
||||
@@ -500,5 +530,6 @@ describe("migration 0355", () => {
|
||||
]);
|
||||
expect(await layout("hero-default")).toEqual({ version: 1, widgets: [] });
|
||||
expect(await layout("hero-none")).toBeNull();
|
||||
expect((await layout("hero-malformed"))?.enabledHeroItemKinds).toBe("all");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user