<feed xmlns='http://www.w3.org/2005/Atom'>
<title>doot.git/internal/handlers, 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-08-24T06:20:52+00:00</updated>
<entry>
<title>Remove confirmed-orphaned routes, handlers, and templates</title>
<updated>2026-08-24T06:20:52+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-24T06:20:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=5c659d9046add8c77bd7699961a910505dcb4bc2'/>
<id>urn:sha1:5c659d9046add8c77bd7699961a910505dcb4bc2</id>
<content type='text'>
From the sitemap audit: /tabs/meals (same dead-tab pattern as the
removed /tabs/conditions, only reachable via ?tab=meals with no nav
button), /partials/lists (superseded by inline .Lists rendering in
trello-board.html), /shopping/toggle and /shopping/mode/{store}/toggle
(superseded by the one-way complete/filter model), plus the orphaned
trello-boards.html and error-banner.html templates that nothing
rendered or included. Rewrote the meals grouping test to exercise
groupMeals() directly since that logic is still live via the timeline.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
</content>
</entry>
<entry>
<title>Remove unused /tabs/conditions partial and route</title>
<updated>2026-08-23T17:43:49+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-23T17:43:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=c46c135f52090a082660cb06131282796f5cd592'/>
<id>urn:sha1:c46c135f52090a082660cb06131282796f5cd592</id>
<content type='text'>
Only the standalone /conditions page was ever meant to exist; the
HTMX tab variant was dead weight, reachable only via an unlinked
?tab=conditions query param with no button in the tab bar.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
</content>
</entry>
<entry>
<title>Replace Google Tasks service-account auth with real OAuth</title>
<updated>2026-08-16T00:03:51+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-16T00:03:51+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=3660486153a16760d2b980e546bbbd29408fb8d4'/>
<id>urn:sha1:3660486153a16760d2b980e546bbbd29408fb8d4</id>
<content type='text'>
Service-account auth structurally cannot see a regular user's personal
task lists (no equivalent of Calendar's per-item sharing model) --
confirmed via GetTaskLists returning exactly the service account's own
empty "My Tasks" list, never the real user's three lists. Zero rows
were ever cached in production as a result.

Adds a standard 3-legged OAuth flow: /settings/google-tasks/connect
redirects to Google's consent screen (AccessTypeOffline+ApprovalForce
so a refresh_token is always issued), /callback exchanges the code and
persists the token (new oauth_tokens table), /disconnect clears it.
GoogleTasksClient now takes an option.ClientOption instead of a
credentials file path; NewGoogleTasksOAuthClient wraps it with a
dbTokenSource that reloads/refreshes from the DB on each access-token
expiry and re-persists -- carefully preserving the original
refresh_token when Google's refresh response omits one (it usually
does), which would otherwise silently and permanently break future
refreshes.

Settings page shows connection status and a Connect/Disconnect
button. Calendar keeps using service-account auth (that one actually
works). Requires a one-time manual step: create an OAuth 2.0 Client ID
in Google Cloud Console and set GOOGLE_OAUTH_CLIENT_ID/SECRET in .env
-- documented in .env.example.
</content>
</entry>
<entry>
<title>Fix integration papercuts found in cross-source review</title>
<updated>2026-08-14T08:12:29+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-14T08:12:29+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=b55cfbbd433bed6035dfa228ee700e2cca060ca4'/>
<id>urn:sha1:b55cfbbd433bed6035dfa228ee700e2cca060ca4</id>
<content type='text'>
getAtomDetails's gtasks case was a stub returning a hardcoded "Google
Task" title, so every gtask completed via the web Tasks/Timeline tab
or the Agent API logged into completed-tasks history with no real
title or due date -- now calls findGoogleTask like every other gtasks
call site already does. Gtasks completion also skipped cache
invalidation in two call sites (HandleCompleteAtom, the Agent API's
handleAgentTaskToggle) that already had it for trello; both now
invalidate CacheKeyGoogleTasks the same way the widget handlers do.
HandleTaskDetailPage (the widget deep-link fallback page) hand
-duplicated loadTaskDetailData's lookup instead of calling it, so it
never got this session's earlier gtasks-detail fix -- now delegates.
Also deleted PlanToEatAPI.GetRecipes, dead code since its introduction
("for Phase 2," never called).

Adds a running .agent/critiques.md tracking structural/process
critiques surfaced in conversation, separate from a concrete plan.
</content>
</entry>
<entry>
<title>Fix blank task-detail modal for Google Tasks</title>
<updated>2026-08-13T08:38:29+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-13T08:38:29+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=a03d7673e7adf9a575c7272b2b528a74b230535e'/>
<id>urn:sha1:a03d7673e7adf9a575c7272b2b528a74b230535e</id>
<content type='text'>
loadTaskDetailData/HandleUpdateTask only had cases for "trello" and
"doot" sources, so opening a gtask from the Tasks or Timeline tab
rendered an empty title/description. Reuses the existing
findGoogleTask cache lookup for both. Renamed the API client's
UpdateTaskNotes to UpdateTask(title, notes) so the web modal can save
an edited title too, not just description; the widget's
description-only edit popup now just round-trips the task's existing
title unchanged.
</content>
</entry>
<entry>
<title>Add task title editing/deletion, timeline click-to-open, widget app launch</title>
<updated>2026-08-12T23:35:47+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-12T23:35:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=2509dde6aa372a505b186657706f4d21bd391807'/>
<id>urn:sha1:2509dde6aa372a505b186657706f4d21bd391807</id>
<content type='text'>
Task-detail modal was description-only with no delete affordance;
HandleUpdateTask now saves the title too and a Delete button hits a new
DELETE /tasks/{id} route backed by store.DeleteNativeTask, which repairs
chain_position/unlocks the successor when the deleted task belongs to a
chain. Timeline tab task/card/gtask rows now open the same detail modal
as the Tasks tab. Android widget's "TODAY" header is now a tap target
that launches DashboardActivity, since nothing previously opened the
full app from the widget.
</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>Wire the Tasks tab into nav; fold in buckets/projects/labels and recurrence</title>
<updated>2026-08-07T10:39:28+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-07T10:39:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=eadd17df2946a1219fdf02f2ee0a0ac19734e56d'/>
<id>urn:sha1:eadd17df2946a1219fdf02f2ee0a0ac19734e56d</id>
<content type='text'>
The Tasks tab (/tabs/tasks) existed server-side and was tested, but
nothing in the nav linked to it -- it was pure dead weight in the other
direction. Wiring it up as the natural home for everything that was
either misplaced in Settings or missing a web UI entirely:

- Maintenance Buckets, Projects, and Labels moved out of Settings and
  into the Tasks tab (restyled from Settings' opaque slate cards to the
  glass/backdrop-blur look already used by the tab's chain/atom cards --
  they're now embedded in index.html's page shell, not a standalone
  page, so the shared bg-card/bg-input classes from that shell apply).
  Settings keeps only what's actually settings: Passkeys, Trusted
  Agents, Data Sources.
- Added a Recurrence section to the task-detail modal (freq/interval/
  weekday form, posting to a new POST /tasks/recurrence -- the HTMX
  counterpart to the widget API's HandleWidgetTaskRecurrence). Native
  task recurrence previously had zero web UI at all, only reachable via
  the Android widget's RecurrenceEditDialog.

Also fixed a real bug found while touching this code: HandleGetTaskDetail's
source switch only had a case for "trello" -- opening any native ("doot")
task's detail modal, which is most tasks in this tab, showed a blank
title and description. Factored both call sites (initial GET and the
re-render after a recurrence edit) through one loadTaskDetailData helper
and added the missing "doot" case. Also fixed task-detail.html's styling,
which was still using pre-dark-theme classes (text-gray-900 etc.) --
functionally invisible text on the modal's dark background.

Verified with a throwaway local server (real templates + real DB, not
the MockRenderer the unit tests use) seeded with a recurring task,
a bucket, a project, and a label -- confirmed all five touched routes
render 200 with the expected content, including the populated
Buckets/Projects/Labels sections and a real weekly-recurrence form
with the correct weekdays pre-checked. Caught and fixed a copy bug
this way too ("every 2 weeklys" -&gt; "every 2 weeks"). Not committed;
deleted after use.

go build ./..., go vet ./..., and go 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 production wedge: propagate context to Google Calendar API calls</title>
<updated>2026-08-04T20:10:25+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-04T20:10:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=0a410243dea33f204764000be81814e541dcae48'/>
<id>urn:sha1:0a410243dea33f204764000be81814e541dcae48</id>
<content type='text'>
Three .Do() calls in google_calendar.go accepted a ctx parameter but
never chained .Context(ctx) into the actual SDK call, so the existing
global 60s request timeout never reached the blocking network call.
One hung Google Calendar request wedged every DB/session-touching
request path in production for three days (2026-08-01 through
2026-08-04), undetected because /health unconditionally returned 200
throughout.

- Wire .Context(ctx) into GetUpcomingEvents, GetEventsByDateRange, and
  GetCalendarList.
- Bound aggregateData's four external fetches with a per-fetch
  sub-context as defense-in-depth (only effective if the callee
  actually honors ctx -- documented as such, not oversold).
- Make GetUpcomingEvents/GetEventsByDateRange fetch calendars
  concurrently instead of sequentially: a review of this fix caught
  that a shared per-fetch deadline over a sequential loop would starve
  calendars past the first under any real latency, silently caching
  partial results as complete. Concurrent fetches give every calendar
  an equal shot at the same deadline instead.
- /health now does a real PingContext DB check instead of a static
  "ok" (Handler.PingDB, tested for both healthy and closed-DB cases).
- Add internal/api/context_audit_test.go: an AST-based structural
  guard that fails any future .Do() call in google_*.go missing
  .Context(...) anywhere in its chain, so this class of bug can't
  silently recur. Verified by deliberately reintroducing the original
  bug against a backup and confirming the guard catches it.
- Add scripts/health-watchdog.sh: cron job restarts the service if
  /health fails twice in a row, five minutes apart.

go test ./... -race is green. Deployed and live-verified.
</content>
</entry>
</feed>
