<feed xmlns='http://www.w3.org/2005/Atom'>
<title>doot.git/cmd/dashboard, 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>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>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>
<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>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>
