<feed xmlns='http://www.w3.org/2005/Atom'>
<title>doot.git/internal/api, 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-16T00:03:51+00:00</updated>
<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>Fix Google Calendar TimeMin using server time instead of display timezone</title>
<updated>2026-08-06T01:50:56+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-06T01:50:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=c8ebaba6e04169359d84e00ffa2d797f40bc077e'/>
<id>urn:sha1:c8ebaba6e04169359d84e00ffa2d797f40bc077e</id>
<content type='text'>
GetUpcomingEvents built its TimeMin cutoff from raw time.Now() (server/UTC
time), not the start of today in the configured display timezone. Google's
API filters TimeMin against each event's END time (exclusive), so for a
UTC-10 display timezone (Pacific/Honolulu) that cutoff landed mid-afternoon
the PREVIOUS Hawaii-local day, not midnight of the actual display day.

Confirmed against live data before fixing, not assumed: the cached
calendar_events table's earliest row was "2026-08-05 15:00:00-10:00" --
exactly the mid-afternoon-yesterday skew this bug produces -- with a
complete gap for all of 2026-08-06 (today). This is what caused two
symptoms reported against the widget: already-ended-today events missing
entirely (never fetched into the cache in the first place, not a
client-side rendering bug -- the 2026-08-05 Android "past events" feature
was built correctly but had nothing to render) and the tomorrow section
appearing empty (same skewed data window scrambling the day-bucketing
downstream).

Fix: compute TimeMin from time.Now().In(displayTZ)'s start-of-day instead.
Added TestGetUpcomingEvents_TimeMinIsStartOfTodayInDisplayTimezone, which
asserts the actual outgoing timeMin query parameter is midnight-in-tz and
lands on today's date -- verified it catches the regression by reverting
to raw time.Now() against a real backup and confirming the exact failure
mode (captured timeMin = mid-afternoon the day before), then restored.

go test ./... -race is green. Deployed. Note: the calendar cache only
refreshes when the web dashboard is visited (aggregateData) -- the widget's
own refresh only re-reads the DB cache, never triggers a live Calendar
resync -- so the currently-cached stale data needs one dashboard visit to
actually reflect this fix, not just the deploy.
</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>fix: Google Tasks due dates shifted a day earlier in negative-offset timezones</title>
<updated>2026-07-28T02:37:23+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-28T02:37:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=2da86b009c76bb7688103da49a9125d35d3a1ed8'/>
<id>urn:sha1:2da86b009c76bb7688103da49a9125d35d3a1ed8</id>
<content type='text'>
Google Tasks' due field is date-only but always serialized as an
RFC3339 timestamp fixed at midnight UTC regardless of the user's
timezone. Converting that instant into displayTZ (dueDate.In(tz))
reinterpreted it as a real moment in time instead of re-anchoring the
same calendar date to local midnight -- midnight UTC becomes 2pm the
previous day in UTC-10 (Pacific/Honolulu), so every dated Google Task
silently landed one day earlier than its actual due date. A task due
tomorrow showed up in today's section.

Fix: extract the Y/M/D from the UTC-anchored timestamp (which IS the
intended calendar date) and rebuild midnight in displayTZ from those
components, instead of converting the instant.
</content>
</entry>
<entry>
<title>feat(widget): tap event to open its source directly, drop detail popup</title>
<updated>2026-07-16T02:42:31+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-13T17:52:10+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=c4066f78d83b1440155248f9bc724b905aa88697'/>
<id>urn:sha1:c4066f78d83b1440155248f9bc724b905aa88697</id>
<content type='text'>
Tapping a calendar event (or any event-type item) now opens the event's
URL (Google Calendar, Plan to Eat, etc.) directly instead of showing an
intermediate popup with an "Open in Calendar" button. Removes
EventDetailActivity and the recurrence-schedule lookup it was the only
consumer of: WidgetRepository.getRecurrence, the Go
/api/widget/recurrence endpoint, HandleWidgetRecurrence,
GoogleCalendarAPI.GetRecurrenceRule, and formatRecurrence, plus their
tests. RecurringEventID itself stays -- it's general calendar-sync
metadata used elsewhere in the timeline pipeline, not exclusive to the
removed popup.
</content>
</entry>
<entry>
<title>Merge github/master: reconcile with parallel widget work</title>
<updated>2026-07-13T07:11:04+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-13T07:11:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=44abf42ed45aa8f285e7ce031cbb9ef1ade667ea'/>
<id>urn:sha1:44abf42ed45aa8f285e7ce031cbb9ef1ade667ea</id>
<content type='text'>
Another session pushed 27 commits in parallel covering quick-add, event
detail popups, recurrence display, overdue badges, a manual refresh button,
and its own fix for the same overdue-tasks bug (via a separate
GetOverdueNativeTasks fetch folded into BuildTimeline, rather than widening
GetNativeTasksByDateRange's bound directly). Reconciled rather than blindly
taking one side:

- Reverted GetNativeTasksByDateRange to its original bounded query and kept
  upstream's GetOverdueNativeTasks + BuildTimeline fold-in as the sole
  overdue mechanism for native tasks, to avoid double-counting overdue
  items (my widened query + their separate fetch would have both returned
  them). Re-pointed the regression test at the now-correct contract and
  added a store-level test for GetOverdueNativeTasks directly.
- Kept my GetGoogleTasksByDateRange fix as-is (single unbounded query) --
  upstream never touched Google Tasks overdue handling, so there's no
  duplication risk there.
- Rewove WidgetRoot's LazyColumn structure (added for scrolling) around
  upstream's new header buttons, pinned all-day event rows, and the
  enhanced TomorrowSection, none of which were written LazyColumn-aware
  since that work landed on this side only.
- Combined both sides' additions to TaskDetailActivity/TaskDetailSheet
  (description-edit detail popup + due-date reschedule label) and
  WidgetRepository/Actions (optimistic local removal + refresh button
  wiring) -- these were independent, non-overlapping features that both
  needed to survive.
- Renumbered the migration collision: both sides independently added a
  migration numbered 022. Card-description was already applied to the live
  production DB under that filename earlier this session (migrations are
  tracked by filename), so it keeps 022; the recurring-event-id migration,
  never deployed under any name here, moves to 023.

Verified: go build clean, full test suite passes (only the two
pre-existing agent-handler failures and the pre-existing models package
build error remain, both confirmed unrelated via git stash before this
session began), and a dry run against a copy of the live production
database applies both migrations cleanly with no re-run conflicts.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>feat(widget): editable task details, scrollable list, and overdue-task fixes</title>
<updated>2026-07-13T06:54:13+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-13T06:54:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=8310f802dd9fc6ef5dff0be7f640f79c5b39987f'/>
<id>urn:sha1:8310f802dd9fc6ef5dff0be7f640f79c5b39987f</id>
<content type='text'>
- Add description editing to the widget's task detail popup for
  doot/gtasks/trello, backed by new GET /api/widget/detail and
  POST /api/widget/update endpoints
- Make Google Tasks and Trello cards completable via the widget (Trello
  completion archives the card); fix Trello description never being
  fetched, which meant saving could silently wipe a card's real desc
- Fix google_tasks.due_date/updated_at (TEXT columns) never round-tripping
  through sql.NullTime, which broke cached Google Tasks reads whenever
  the cache was valid
- Fix native-task and Google-Task date-range queries excluding anything
  due before the window start, which dropped incomplete tasks off the
  widget the moment their due day passed (the "overdue tasks disappeared"
  bug)
- Fix native task description edits blanking the task's title
- Make the widget's day list scroll (LazyColumn) instead of clipping
- Optimistically remove a task from the widget immediately on completion,
  ahead of the authoritative background refresh

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>feat(widget): add recurrence lookup endpoint (GET /api/widget/recurrence)</title>
<updated>2026-07-12T11:12:55+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-12T11:12:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=6fdd09bfe030ce91ee188f18c198fd6a57c7b42f'/>
<id>urn:sha1:6fdd09bfe030ce91ee188f18c198fd6a57c7b42f</id>
<content type='text'>
</content>
</entry>
</feed>
