Fix plan days with no chapters, add a way out of ink mode, harden ntfy reminders
Build and Push Docker Image / build-and-push (push) Failing after 19s

Plans can no longer have more days than chapters (existing ones heal), and
opening a plan never strands the Reader on an empty day. The Reader shows
Done drawing while inking (Escape also exits). Reminders use the user's own
timezone, send within a 3 hour grace window, and Settings has a Send test
notification button.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:34:46 -04:00
1 parent ecb1c701fa
commit 297f2c7e84
14 files changed
+335 -58

No files matched your search

+21 -1
View File
@@ -180,6 +180,26 @@ export function getPlan(planId) {
return PLAN_BY_ID[planId] ?? null;
}
// A plan can't have more days than it has chapters (the extra days would be empty, with
// nothing to read). Existing custom plans with too many days heal to one chapter a day.
export function clampPlanDays(books, chapterRanges, days) {
const total = (books ?? []).reduce((sum, b) => sum + bookChapterCount(b, chapterRanges), 0);
const wanted = Math.max(1, Math.round(Number(days) || 1));
return total > 0 ? Math.min(wanted, total) : wanted;
}
export function normalizeCustomPlan(plan) {
const days = clampPlanDays(plan.books, plan.chapterRanges, plan.days);
return days === plan.days ? plan : { ...plan, days };
}
// Nearest day at or after `from` that has something to read, else the nearest one before it.
export function nearestReadingDay(schedule, from) {
for (let i = Math.max(0, from); i < schedule.length; i += 1) if (schedule[i]?.length) return i;
for (let i = Math.min(from, schedule.length) - 1; i >= 0; i -= 1) if (schedule[i]?.length) return i;
return -1;
}
// Registers/updates/removes user-created plans in the lookup used by getPlan/getPlanSchedule.
// Called by useReadingPlans() whenever the custom-plans list changes.
export function registerCustomPlans(customPlans) {
@@ -190,7 +210,7 @@ export function registerCustomPlans(customPlans) {
}
}
for (const cp of customPlans) {
PLAN_BY_ID[cp.id] = { category: 'My Plans', icon: '✨', ...cp, custom: true };
PLAN_BY_ID[cp.id] = { category: 'My Plans', icon: '✨', ...normalizeCustomPlan(cp), custom: true };
scheduleCache.delete(cp.id);
}
}