diff --git a/README.md b/README.md
index 078218f..feef4dc 100644
--- a/README.md
+++ b/README.md
@@ -68,8 +68,10 @@ remain manageable on days off, while their checkboxes stay disabled.
Mutations are saved through the authenticated API; failed
saves retain the recorded values and allow retry. Account and calendar failures
have retry states. The dashboard refreshes on window focus and every minute while
-visible, using the server’s date and saved account timezone. Historical editing
-and combined-chart management are not exposed on this homepage.
+visible, using the server’s date and saved account timezone. Select a calendar date and choose **Edit selected check-in** to correct counts,
+checkboxes, or task occurrences using that date’s saved requirements. A date picker
+also reaches history outside the displayed year. Future dates and days off are read-only.
+Combined-chart management is not exposed on this homepage.
For browser QA without changing application data, run
`bun scripts/dashboard-preview.ts` and visit
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 46a5848..a5e0850 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -116,6 +116,23 @@ const buttonNamed = (name: string) => [...container.querySelectorAll button.textContent === name || button.getAttribute("aria-label") === name)!;
describe("homepage", () => {
+ test("historical check-ins use the selected date's tracking method and preserve today", async () => {
+ const f = connectAccount();
+ f.setTime("2026-09-03T12:00:00Z");
+ const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
+ f.setTime("2026-09-04T12:00:00Z");
+ await f.json(`/habits/${habit.id}`, "PATCH", { method: "count", target: 10, unit: "pages" });
+ await render("/");
+ await act(async () => container.querySelector('[aria-label^="2026-09-03:"]')!.click());
+ await act(async () => buttonNamed("Edit selected check-in").click());
+ const past = [...container.querySelectorAll('input[type="checkbox"]')].find(input => input.closest("label")?.textContent === "Completed on this date")!;
+ expect(past).toBeDefined();
+ await act(async () => past.click());
+ expect((await f.json(`/habits/${habit.id}/days/2026-09-03`)).complete).toBe(true);
+ expect((await f.json(`/habits/${habit.id}/days/2026-09-04`)).value).toBe(0);
+ expect(container.textContent).toContain("Saved check-in for 2026-09-03");
+ });
+
test("signed-out visitors can try progress without calling private habit APIs", async () => {
await render("/");
expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm.");
diff --git a/src/components/CheckInEditor.tsx b/src/components/CheckInEditor.tsx
new file mode 100644
index 0000000..1740095
--- /dev/null
+++ b/src/components/CheckInEditor.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from "react";
+import { habitRequest, type TodayHabit } from "../lib/dashboard";
+import { Button, Checkbox } from "./design-system/primitives";
+import { Field } from "./design-system/Field";
+
+export function CheckInEditor({ habitId, date, disabled, onSaved, onExpired }: {
+ habitId: string; date: string; disabled?: boolean; onSaved: () => void; onExpired?: () => void;
+}) {
+ const [day, setDay] = useState(null);
+ const [count, setCount] = useState("");
+ const [error, setError] = useState("");
+ const [notice, setNotice] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [attempt, setAttempt] = useState(0);
+ const saving = useRef(false);
+ const expired = useRef(onExpired); expired.current = onExpired;
+ useEffect(() => {
+ const controller = new AbortController();
+ setDay(null); setError("");
+ habitRequest(`/habits/${habitId}/days/${date}`, { signal: controller.signal })
+ .then(value => { if (!controller.signal.aborted) { setDay(value); setCount(String(value.value)); } })
+ .catch(error => { if (!controller.signal.aborted) {
+ if (error.message.includes("session has expired")) expired.current?.();
+ setError(error.message);
+ } });
+ return () => controller.abort();
+ }, [habitId, date, attempt]);
+ async function save(body: { count: number } | { done: boolean }, taskId?: string) {
+ if (saving.current || disabled) return;
+ saving.current = true; setBusy(true); setError(""); setNotice("");
+ try {
+ const result = await habitRequest(`/habits/${habitId}/days/${date}/${taskId ? `tasks/${taskId}` : "progress"}`, {
+ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
+ });
+ setDay(result); setCount(String(result.value)); setNotice(`Saved check-in for ${date}.`); onSaved();
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Could not save. Try again.";
+ setError(message); if (message.includes("session has expired")) expired.current?.();
+ } finally { saving.current = false; setBusy(false); }
+ }
+ return
+
Check-in · {date}
+ {error &&
{error}
}
+ {!day ? error ?
:
Loading check-in…
+ : day.future ?
Future check-ins cannot be edited.
+ : !day.due ?
Nothing was scheduled for this date.
+ : <>
+
{day.name} · {day.timezone}. Changes use the requirements saved for this date.
+ {day.method === "count" ?
: day.method === "manual" ?
void save({ done: event.target.checked })} />
+ : day.tasks.map(task => void save({ done: event.target.checked }, task.taskId)} />)}
+ >}
+ {notice}
+ ;
+}
diff --git a/src/components/HabitHistory.tsx b/src/components/HabitHistory.tsx
index faab3b6..cd4f4c9 100644
--- a/src/components/HabitHistory.tsx
+++ b/src/components/HabitHistory.tsx
@@ -7,6 +7,8 @@ import { Button } from "./design-system/primitives";
import { HabitChart } from "./design-system/HabitChart";
import { CalendarLegend } from "./design-system/CalendarLegend";
import { shade } from "../habits/calendar";
+import { CheckInEditor } from "./CheckInEditor";
+import { Field } from "./design-system/Field";
import { ItemActions } from "./design-system/ItemActions";
export function HabitHistory({
@@ -20,6 +22,7 @@ export function HabitHistory({
tasks,
editor,
onExpired,
+ onHistorySaved,
}: {
habit: TodayHabit;
date: string;
@@ -31,7 +34,10 @@ export function HabitHistory({
tasks?: ReactNode;
editor?: ReactNode;
onExpired?: () => void;
+ onHistorySaved?: () => void;
}) {
+ const [selectedDate, setSelectedDate] = useState();
+ const [editingDay, setEditingDay] = useState(false);
const [calendar, setCalendar] = useState(null);
const [error, setError] = useState("");
const [attempt, setAttempt] = useState(0);
@@ -106,7 +112,10 @@ export function HabitHistory({
Loading your habit history…
) : (
+ <>
}
/>
+
+
+
+ {editingDay && <>
+ {id => { if (event.target.value) setSelectedDate(event.target.value); }} />}
+ { setAttempt(value => value + 1); onHistorySaved?.(); }} />
+ >}
+ >
);
return (
voi
date={today.date}
revision={revision}
disabled={busy || loading || needsRefresh}
+ onHistorySaved={() => setAttempt(value => value + 1)}
onUpdate={update}
onManage={manage}
onExpired={() => expired.current()}
@@ -454,6 +455,7 @@ function SavedHabit({
disabled,
onUpdate,
onManage,
+ onHistorySaved,
onExpired,
}: {
habit: TodayHabit;
@@ -466,6 +468,7 @@ function SavedHabit({
taskId?: string
) => Promise;
onExpired: () => void;
+ onHistorySaved: () => void;
onManage: (
habit: TodayHabit,
patch: Record | null,
@@ -483,6 +486,7 @@ function SavedHabit({
revision={revision}
disabled={disabled}
onExpired={onExpired}
+ onHistorySaved={onHistorySaved}
onEdit={(color) => setEditing({ mode: "edit", color })}
onDelete={() => setEditing({ mode: "delete", color: "#196127" })}
editor={