| Age | Commit message (Collapse) | Author |
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
Quick Add's text field never requested focus or triggered the keyboard,
so tapping it opened a sheet with no visible way to type. Added a
FocusRequester + delayed requestFocus()/keyboard show (ModalBottomSheet
needs a beat to finish its enter animation before focus requests land).
While in there, rebuilt the quick-add sheet to match the task edit
popup instead of being a bare title field: due date, recurrence,
project, and label chips, plus a description field, reusing the same
dialogs the edit popup already uses. POST /api/widget/add now returns
the created task's id so the client can chain the same
project/labels/recurrence/due-date setter calls edit already relies on.
|
|
Task 11 of the task-budgets-and-availability plan: reuse the
computeBudgetStatus helper (Task 8) in HandleTimeline so the dashboard's
Today section header shows scheduled/available minutes when a
budget-tracked task exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
|
|
Adds 6 widget HTTP handlers (availability get/create/delete, task
estimate, project and label budget-tracked toggles) plus route
registration, following the existing widget handler conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
Wires ComputeBudgetPeriod together with availability blocks, tracked
project/label sets, calendar events, and native tasks into a new
computeBudgetStatus helper on *Handler. HandleWidgetGet now populates
WidgetResponse.BudgetStatus with today/week periods, but only when at
least one incomplete budget-tracked task exists in the rolling week
window -- otherwise the field is omitted entirely so unconfigured users
see no new UI. Errors are logged and swallowed, matching the existing
resilience pattern for this widget endpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
task-filter doc comment; add multi-day test
Overlapping calendar events within the same availability block were each
subtracted independently, double-counting their intersection and making
availability under-counted (a 120-min block with two 60-min-overlap events
that overlap each other 18:30-19:00 came out to 0 min instead of the
correct 30 min). Replace the per-event overlapMinutes subtraction with
busyMinutesInBlock, which clips each event to the block, merges the
clipped intervals, and subtracts the union's total length.
Also clarify ComputeBudgetPeriod's doc comment: task filtering only
enforces an upper bound (due < end); start is intentionally not used to
filter tasks, since callers are expected to pre-fetch and pass in
already-overdue tasks. Add a multi-day-window test verifying availability
sums correctly across days, a block only contributes on its matching
weekday, and the day-iteration loop excludes the day at `end`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
ComputeBudgetPeriod is a standalone, dependency-free function over
in-memory availability blocks, calendar events, and tasks: sums weekly
availability minus overlapping calendar events for [start, end), and
sums estimated minutes for tracked, incomplete tasks due in that window.
|
|
|
|
flag
|
|
|
|
|
|
Fix formatting and alignment violations: align struct field tags in Task,
realign Weekday comment in AvailabilityBlock, and remove extra blank line
before CalendarEvent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session
|
|
Add EstimatedMinutes to Task, BudgetTracked to Project and LabelColor
structs. Create budget.go with AvailabilityBlock, BudgetPeriod, and
BudgetStatus types for modeling task budgets and calendar availability.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
Creates migrations/025_task_budgets_and_availability.sql with:
- New availability_blocks table for manual weekly availability templates
- estimated_minutes column on native_tasks for time budgeting
- budget_tracked flag on projects and labels for opt-in tracking
Fixes pre-existing bug in sqlite_test.go's setupTestStoreWithNativeTasks where
project_id column was missing from the schema, causing test failures.
Updates all hand-rolled test helpers (sqlite_test.go, native_tasks_test.go)
to include the new columns, and adds availability_test.go with newAvailabilityTestStore
helper for Task 4's availability CRUD tests.
All store tests pass, including the two that were previously failing
(TestGetNativeTasksByDateRange_ExcludesOverdue, TestGetOverdueNativeTasks_IncludesOnlyPastDue).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
|
|
HandleAgentTaskCreate decoded project_id from the request but never
forwarded it to CreateNativeTask, so a task created via the external
Agent API with a project assigned would silently create the task
unassigned instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Tests verify ProjectColor is populated in BuildTimeline and copied to WidgetItem.
|
|
Projects are fetched once per BuildTimeline call into a color lookup
map, not queried per-task -- same pattern as RecurringEventID's
existing TimelineItem->WidgetItem threading.
|
|
Tests for HandleWidgetProjectsGet, HandleWidgetProjectsCreate,
HandleWidgetTaskProject, HandleWidgetTaskLabels, HandleWidgetLabelsGet,
HandleWidgetLabelsColorSet, and the extended HandleWidgetTaskDetail
response with project and labels fields.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Extends the task-detail response with project/labels so the popup can
display them without a second round trip.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
|
|
|
|
project_id joins the existing labels JSON column as series-level
metadata: CreateNextIteration copies both onto every new recurring
occurrence, matching content/description/priority's existing behavior.
|
|
Pure move: all agent-session and agent CRUD/trust methods (~340 lines)
plus their full test suite and setupTestStoreWithAgents helper, out of
sqlite.go/sqlite_test.go into agents.go/agents_test.go. No behavior
change. Mirrors internal/handlers/agent.go's domain naming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Pure move: SaveCalendarEvents/GetCalendarEvents/GetCalendarEventsByDateRange
into calendar.go, SaveGoogleTasks/GetGoogleTasks/GetGoogleTasksByDateRange
into google_tasks.go. Neither cluster had dedicated tests in
sqlite_test.go to move. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Pure move: UserShoppingItem type, its CRUD methods, and
SetShoppingItemChecked/GetShoppingItemChecks, plus their tests and
setupTestStoreWithShopping helper, out of sqlite.go. No behavior
change. Mirrors the existing internal/handlers/shopping.go naming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Pure move: SaveMeals/GetMeals/GetMealsByDateRange and their test
(plus its setupTestStoreWithMeals helper, used only by that test) out
of the 1400+ line sqlite.go into their own file. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Task.IsRecurring/Atom.IsRecurring were never assigned anywhere in the
codebase (confirmed via full-repo search), so PartitionAtomsByTime's
"hide future recurring tasks until due" branch was permanently dead.
RecurrenceSeriesID is the real recurring indicator now; wiring up
"hide future recurring successors" as an actual feature is a separate
design decision, not part of this cleanup pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
- SourceMeal/TypeMeal (AtomSource/AtomType): leftover from the removed
MealToAtom conversion (b2d8fc4); zero references anywhere else.
- HandleGetSourceOptions: never registered as a route, and its own
test comment already noted "may fail if template not found, which
is acceptable" -- the settings-source-options template it rendered
doesn't exist in web/templates/ at all. Fully unreachable/broken,
not just unused.
go build/vet/test all clean, no behavior change for anything reachable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Two independent pre-existing failures, both from incomplete refactors:
- agent.go's four task write/create switches (complete/uncomplete,
update due date, update details, create) never got a "doot" case
added when the Todoist integration was removed (945c345) in favor
of native tasks -- the test file was updated to use source=doot,
but the handlers themselves still only recognized "trello"/"gtasks",
so every native-task Agent API call 400'd with "Unknown source".
getAtomDetails already had a "doot" case, confirming this was an
incomplete migration, not an intentional gap.
- TestMealToAtom in atom_test.go tested MealToAtom, a function removed
from atom.go months earlier (b2d8fc4) when meals were dropped from
the unified Atom/timeline system; the Meal struct itself is still
used elsewhere (shopping/meals feature) but no longer produces atoms,
and the test was never cleaned up to match.
go test ./internal/... ./cmd/... is now fully green with no failures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Folds the "is this still the latest row in its series?" check directly
into the INSERT as a single atomic INSERT...SELECT...WHERE NOT EXISTS
statement, instead of a separate SELECT-then-INSERT sequence. Closes a
narrow race where the completion trigger and the periodic due-date
check could both pass their "still latest" check before either had
inserted, producing two duplicate successor rows for the same series.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
HandleWidgetTaskUpdate now returns 404 instead of silently succeeding
when the widget's cached task id no longer exists, matching every
other native_tasks mutator's checkRowsAffected pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
GET /api/widget/task, POST /api/widget/task/update,
POST /api/widget/task/recurrence, POST /api/widget/task/next-date.
Doot-only; the recurrence/next-date fields are null in the detail
response for a non-recurring task.
|
|
A recurring task's successor now also gets created once its due date
passes, independent of completion -- an ignored/overdue recurring task
no longer blocks the next occurrence from appearing. Runs every 15
minutes via a new goroutine in main.go, cancelled on shutdown.
|
|
CompleteNativeTask now creates a new row for the next occurrence when
completing the latest iteration of a recurring series (using the
one-shot next_occurrence_override if set, else ComputeNextOccurrence).
Completing an already-superseded row (the periodic due-check beat it
to creating the successor) just marks it completed, no double-create.
|
|
Implement pure function to compute next occurrence of recurring tasks given
frequency, interval, and optional weekday constraints. Supports daily, weekly,
monthly, and yearly recurrence patterns. Weekly recurrence with weekday
constraints wraps to next applicable week when needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
recurrence_series_id != "" is the real recurring indicator (IsRecurring
predates this feature and is never set). Adds parseWeekdays/formatWeekdays
for the comma-separated weekday-list column, and threads the five new
columns through scanNativeTasks and all four existing SELECT queries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Renders docs/superpowers/{specs,plans}/*.md to HTML via goldmark,
served from doot's own web server behind the existing session auth
instead of as raw files. DOCS_DIR is configurable (defaults to
docs/superpowers relative to the working directory) so the deployed
server can point straight at the working repo and stay live-synced
with no separate copy/deploy step for new docs.
|
|
A single-day all-day event's End.Date is one calendar day past its own
day (Google Calendar convention, already documented by
TestParseEventTime_AllDayEvent). isMultiDayEvent/multiDayVariant didn't
adjust for this, so every all-day event -- including single-day ones --
was misclassified as multi-day and duplicated across Today and
Tomorrow.
|
|
TimelineItemView wraps a TimelineItem with a per-render-day
MultiDayVariant (starts/ends/spans/none). HandleTimeline's bucketing now
places a multi-day event into both TodayItems and TomorrowItems when it
touches both, instead of only the list matching its start day.
|
|
A genuine all-day Google Calendar event spanning multiple days (e.g. a
3-day conference) never got its EndTime forwarded to the widget client,
since the conversion only set End when !IsAllDay. The client has no way
to detect a multi-day span without both Start and End.
|
|
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.
|
|
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|