<feed xmlns='http://www.w3.org/2005/Atom'>
<title>doot.git/internal/models, branch master</title>
<subtitle>doot — personal productivity web app
</subtitle>
<id>https://git.terst.org/doot.git/atom?h=master</id>
<link rel='self' href='https://git.terst.org/doot.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/'/>
<updated>2026-09-03T02:34:25+00:00</updated>
<entry>
<title>Make the day's list a pull of committed work, not a push of everything unblocked</title>
<updated>2026-09-03T02:34:25+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-03T02:34:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=1477ee531045388de9e7e797200e5cd14e4a6d0e'/>
<id>urn:sha1:1477ee531045388de9e7e797200e5cd14e4a6d0e</id>
<content type='text'>
due_date had been carrying two incompatible meanings: "this has a real
deadline" (Bills, meds, a signup cutoff) and "this is when I intend to
work on it." Chains leaned entirely on the second -- every step was
stamped due_date = now the moment it unlocked -- so a five-minute call
and a six-month certification both arrived looking overdue, and the only
way to quiet them was to hand-reschedule. The list therefore always
contained work that could not be finished that day, which made ending a
day at zero impossible by construction rather than by effort.

Splits the two apart:
  - due_date keeps only its real meaning: an externally-imposed deadline.
  - committed_date (new, migration 031) means "I chose this for today."

Chain steps no longer get a due date on unlock (CreateChain, advanceChain,
and the delete-promotion path), and are excluded from the undated list so
they don't simply fall through to it instead. They reach the day only by
being committed, via a "+ today" control on the chain card. Bucket-active
items get the same control. BuildTimeline pulls in committed tasks,
deduped against tasks already present for a different reason (committed
and due-today are two reasons for one row, not two rows).

Migration 031 also clears the auto-stamped due dates from open chain
steps; completed steps keep theirs as historical record.

Two supporting changes made while in here:
  - Extracted the native_tasks SELECT list into nativeTaskColumns. It had
    been copy-pasted into nine queries that must each agree exactly with
    scanNativeTasks' field order; adding committed_date meant editing all
    nine correctly or silently breaking whichever was missed.
  - Extracted templateFuncMap() and added TestTemplates_AllParse. New()
    only logs a warning when template parsing fails, so a malformed
    template shipped silently and surfaced as a broken page at runtime.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Revert "Show one row per recurring chore in the flat task list"</title>
<updated>2026-09-03T00:42:09+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-03T00:42:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=96518adb706b65670508b170b50f4bd5e1aeeed8'/>
<id>urn:sha1:96518adb706b65670508b170b50f4bd5e1aeeed8</id>
<content type='text'>
This reverts commit 10f9fcd271bbac4ff26f32880db212b5bd3b7f00.
</content>
</entry>
<entry>
<title>Show one row per recurring chore in the flat task list</title>
<updated>2026-09-03T00:40:47+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-03T00:40:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=10f9fcd271bbac4ff26f32880db212b5bd3b7f00'/>
<id>urn:sha1:10f9fcd271bbac4ff26f32880db212b5bd3b7f00</id>
<content type='text'>
The recurrence scheduler materializes a series' next instance once the
current one comes due, regardless of whether it was completed -- that's
deliberate, so ignoring an occurrence doesn't stall the series. The side
effect is that the flat task list shows an unfinished chore and its
already-generated successor at the same time: "Bills" appears twice right
now (Sep 1 overdue + Oct 1), as does "Bones's meds".

CollapseRecurringSeries keeps only the earliest-due open instance per
series, which is the one actually owed. Applied to the atoms list only --
the timeline and widget are date-anchored, where each instance correctly
belongs to its own day, so they keep all of them.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Fix widget showing tomorrow's dated tasks under today</title>
<updated>2026-08-10T03:16:39+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-10T03:16:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=9a3ece4fef431c2c35af239f3df6eeaa10ddeaf4'/>
<id>urn:sha1:9a3ece4fef431c2c35af239f3df6eeaa10ddeaf4</id>
<content type='text'>
Root cause: doot-native and Google Tasks due dates are always
midnight-anchored (even when a task genuinely has a due date), which trips
TimelineItem.ComputeDaySection's "midnight means no specific time" heuristic
and sets IsAllDay=true on them. TimelineItemToWidgetItem then left wi.Start
nil for any Task/GTask with IsAllDay=true, and the Android client's
undated/floating pool (DootWidget.kt's `floating` list) has zero per-day
awareness -- it just packs items forward from "now" -- so a task due
tomorrow rendered as if due today. This was previously diagnosed and
deliberately set aside (see project_doot_isallday_midnight_bug memory) with
a simpler proposed fix (gate ComputeDaySection's heuristic to Event/Meal
types); re-verifying that plan against the actual code before implementing
it surfaced a real problem with it: it would also flip IsAllDay to false for
same-day dated tasks, moving them from the web's untimed-item strip into the
hourly grid with a fabricated "12:00 AM" time label -- fixing the widget by
breaking the web's currently-correct rendering.

Actual fix: added TimelineItem.Undated, a signal genuinely independent of
IsAllDay -- true only for the caller's two genuinely-dateless constructions
(nativeUndated tasks, gtasks with no due date), both of which already use
Time=now as a layout placeholder rather than a real date. ComputeDaySection
and IsAllDay are untouched, so web rendering is unaffected. This gate.go
change routes on that new signal instead: dated Task/GTask items now get
wi.Start populated (so the Android client's existing, already-correct
Start-based day bucketing runs for them) while genuinely undated ones keep
today's nil-Start floating treatment exactly as before.

Also fixed the same latent bug in wi.DueDate's guard while in the same
code path: it only checked !Time.IsZero(), but nativeUndated's Time=now
placeholder is non-zero, so an undated task's Android detail popup would
have shown a fabricated due date of whatever moment the request happened
to run. Now guards on !Undated too.

Verified: added TestTimelineItemToWidgetItem_DatedGTaskGetsStart and
TestTimelineItemToWidgetItem_UndatedTaskWithPlaceholderTime_NilDueDate,
and updated the three existing tests that had encoded the bug's old
"Start stays nil for any IsAllDay task" assumption as expected behavior
(TestTimelineItemToWidgetItem_Task, _DootTaskGetsDueDate,
_AllDayTask_KeepsFloatingBehavior -- the last one needed Undated: true
added since it no longer implies undated on its own). Proved the fix by
reverting internal/handlers/widget.go to the pre-fix condition against a
real backup and confirming exactly the three tests exercising the new
behavior fail, then restored from that backup and confirmed byte-identical.
go build/vet/test all clean.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>Remove the feature toggle system (dead code)</title>
<updated>2026-08-06T18:19:27+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-06T18:19:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=06450fe69ade2928deb9274bb67b7ba60d394b4f'/>
<id>urn:sha1:06450fe69ade2928deb9274bb67b7ba60d394b4f</id>
<content type='text'>
Audited it (couldn't query the live DB directly -- auto-mode classifier
blocks direct production reads without prior approval -- so this is a
code-only audit): GetFeatureToggles/SetFeatureEnabled/IsFeatureEnabled/
CreateFeatureToggle/DeleteFeatureToggle had exactly one caller each, all
inside their own CRUD handlers. Nothing anywhere else in the codebase
read a toggle's Enabled state to gate any actual behavior -- confirmed
by grepping every remaining .Enabled/IsFeatureEnabled reference back to
either this dead code or its own tests. It was pure UI-managed CRUD with
no consumer, unlike Trusted Agents (wired into agent.go/websocket.go)
or Data Sources (wired into the sync pipeline) which stayed.

Removes the Settings page section, the three /settings/features* routes
and handlers, the five Store methods, the FeatureToggle model, and adds
028_drop_feature_toggles.sql (next free migration number, per this
repo's convention of never renumbering -- see 021_drop_tasks.sql for
the same drop-table-forward pattern) to drop the now-unused table.
Also removed the now-dead tests for all of the above.

go build ./... and go test ./... both clean.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>fix: undated tasks were landing in the widget's scheduled-events grid</title>
<updated>2026-07-25T20:09:42+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-25T20:09:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=7f8cd6bfb894385b9ae310fd000cb921fef71cea'/>
<id>urn:sha1:7f8cd6bfb894385b9ae310fd000cb921fef71cea</id>
<content type='text'>
ComputeDaySection unconditionally recomputed IsAllDay from a
midnight-time heuristic, clobbering the IsAllDay:true flag callers set
to mark floating (no-due-date) tasks -- since undated tasks use Time =
now, this silently reset IsAllDay to false and gave them a real Start,
so the widget could render them via EventBlock/HourRow instead of
TaskRow. Their click handler always opens the calendar/source URL, so
tapping a no-due-date task (e.g. a Google Task) opened Google Calendar
instead of the task detail sheet.

ComputeDaySection now only ever sets IsAllDay true, never clears an
already-true value. Also flag IsAllDay for undated Google Tasks, which
never got it set at all (only native undated doot tasks did).
</content>
</entry>
<entry>
<title>Add bucket CRUD and read-only Projects/Labels to Settings page</title>
<updated>2026-07-18T09:38:26+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-18T09:38:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=92909ebed5df68908f32c899de1e480f8d7fb01f'/>
<id>urn:sha1:92909ebed5df68908f32c899de1e480f8d7fb01f</id>
<content type='text'>
New "Maintenance Buckets" section: create a bucket, add a pool item by
title (creates the task and assigns it in one step), remove an item,
delete a bucket (unbuckets its tasks rather than deleting them).

New read-only Projects and Labels sections (name + color swatch) --
both are simple enough that read-only is the right call on web, per
user direction, rather than duplicating the Android popup's editing
UX.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>Rework Tasks tab: Chains section, checklist modal, project visibility</title>
<updated>2026-07-18T00:14:45+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-18T00:14:45+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=f08f06bef47aac2c9effb4cec650d99c2deb2dd7'/>
<id>urn:sha1:f08f06bef47aac2c9effb4cec650d99c2deb2dd7</id>
<content type='text'>
The flat Tasks-tab atom list was silently dumping every chain step
(locked and unlocked) and dormant bucket-pool items in as ordinary
undated cards, with no chain/project context and no protection against
completing a locked step out of order.

- CompleteNativeTask now rejects completing a locked chain task
  (ErrChainTaskLocked), mapped to 400 in both the widget and web
  complete-atom handlers.
- Chain tasks and dormant bucket items are excluded from the flat atom
  list; a new "Chains" section shows one card per active/paused chain
  with the current step and N/M progress.
- New chain checklist modal (GET /chains/{id}) lists every position in
  order with pause/resume/abandon -- the web view originally deferred
  as Android-only.
- Fixed a real bug this surfaced: resuming a paused chain only flipped
  the status flag, never unlocking the deferred successor, so a chain
  paused right after a completion stayed stuck forever. SetChainStatus
  now catches up the deferred advancement on resume, idempotently.
- Atom cards gained a project-name chip for general visibility.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>Extend chain creation to accept per-task description and priority</title>
<updated>2026-07-17T22:46:18+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-17T22:46:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=be4d606e9a1f5b068abcc21bbac58d1e4705ea1f'/>
<id>urn:sha1:be4d606e9a1f5b068abcc21bbac58d1e4705ea1f</id>
<content type='text'>
CreateChain and POST /api/widget/chains previously only took bare
title strings, with priority hardcoded to 1 -- narrower than the
spec's "an ordered list of task titles/descriptions" API line. Now
accepts []models.ChainTaskInput{Content, Description, Priority} per
position, priority defaulting to 1 when omitted.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>Implement linear task chains and recurring maintenance buckets</title>
<updated>2026-07-17T22:22:37+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-17T22:22:37+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=b007fee8fb5b39a5f9b369c59af71ac9e795ceaf'/>
<id>urn:sha1:b007fee8fb5b39a5f9b369c59af71ac9e795ceaf</id>
<content type='text'>
Backend, web timeline, and Android widget wiring for the last two
unimplemented items from doot-future-task-scheduling-ideas.

Chains: task_chains table + chain_id/chain_position/chain_unlocked on
native_tasks (migration 026), WIP-limit-1 advancement hooked into
CompleteNativeTask, locked tasks excluded from all date-based queries,
5 new /api/widget/chains* endpoints, an N/M position badge on web and
Android widget rows.

Buckets: maintenance_buckets table + bucket_id/bucket_state/
bucket_last_active_at on native_tasks (migration 027),
staleness-then-priority selection scoring, a new RunBucketCycleCheck
scheduler loop, 5 new endpoints including the distinct Defer action, a
Defer button on web and Android widget rows.

Also corrected stale "not yet approved" status headers on the two
already-shipped specs this work depended on (labels/projects, budgets/
availability) -- their headers were never updated after implementation.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
</feed>
