summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-08-06Widget: real launcher icon, fix double title bar, rename app to "doot"Peter Stone
Icon: adaptive-icon vector launcher icon reusing web/static/favicon.svg's brand mark (indigo->purple gradient square, white checkmark) so the app and web dashboard visually match. minSdk 36 (per build.gradle.kts) means mipmap-anydpi-v26 alone is sufficient, no legacy PNG fallback needed. Double title bar: DashboardActivity inherited the app-wide Theme.DeviceDefault.DayNight, which has a native ActionBar, stacked on top of the new Compose TopAppBar from the last commit -- visibly two bars, showing "Doot" (static ActionBar label) above "Personal Dashboard" (the Compose bar tracking the web page's own <title>). Added Theme.Dashboard (NoActionBar) for the activity, and stopped tracking the WebView's document.title for the Compose bar's title text -- the dashboard is a single HTMX-swapped page (see DashboardActivity's class doc), so the title never meaningfully changes, and it was just producing a second, differently-branded piece of text. Renamed the app from "Doot Widget" to "doot" throughout (application label, widget-picker description, Settings screen heading) to match the project's actual branding. Also: the web Settings page's "Back to Dashboard" link was a plain <a href="/">, which in the WebView pushes a NEW history entry for "/" instead of reusing the one already on the stack -- so Settings <-> Home round trips kept growing the WebView back-stack ("zigzag"), and the in-app back arrow/hardware back key never actually unwound it. Now it calls history.back() when there's stack to pop, falling back to a plain navigation only if there isn't (e.g. Settings opened directly). Verified on emulator-5556: rebuilt and reinstalled, confirmed a single title bar (no native ActionBar behind the Compose one), confirmed the launcher icon renders (checked via Settings > App info, since this AVD's launcher doesn't expose an app drawer over adb), no crashes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-08-06Widget: give Dashboard its own launcher icon, add toolbar navPeter Stone
DashboardActivity is now the app's single home-screen launcher entry (MAIN/LAUNCHER intent-filter), replacing SettingsActivity in that role. SettingsActivity keeps only APPWIDGET_CONFIGURE (still exported=true, since the launcher/home-screen process starts it directly during widget placement) and is now reached via a gear button in Dashboard's toolbar. DashboardActivity itself is rewritten from a bare setContentView(webView) to Compose Scaffold/TopAppBar wrapping the WebView via AndroidView, adding: - a back arrow (shown only when the WebView has history) instead of relying solely on the hardware back key - a settings gear action launching SettingsActivity - the page title tracked from the loaded page Also fixes a race in the original version: server URL was loaded via a lifecycleScope coroutine racing the WebView's own initialization order. Now it's plain Compose state (LaunchedEffect + AndroidView's update callback), so the WebView never loads before the URL is known. No native tab bar: web/templates/index.html's tabs are HTMX partials (hx-get targeting #tab-content), not separate pages, so loading "/" in the WebView already gets full in-app navigation for free. Verified on emulator-5556 (doot_test_api36): pm resolve-activity confirms DashboardActivity is the launcher default; launched it, confirmed no crash and topResumedActivity is Dashboard; configured a dummy server URL via Settings and confirmed the toolbar renders (title, gear icon) and the gear button correctly navigates to SettingsActivity (topResumedActivity becomes SettingsActivity) with no crash. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-08-06Widget: fix QuickAdd keyboard-focus race, force widget re-render on savePeter Stone
QuickAddActivity's autofocus used delay(150) then requestFocus() + keyboard.show() -- a fixed delay racing against the window actually gaining focus. This is a translucent, separate-taskAffinity popup activity, so window focus transfer is variable/device-dependent; a show() call made before the window is focused is silently dropped by the IME service, no error, no retry -- the classic cause of "keyboard doesn't appear until I tap out and back in" (reported 2026-08-06, "more consistent here" than the same general Android quirk elsewhere). Fixed to react to LocalWindowInfo.isWindowFocused instead of guessing a timing window, plus windowSoftInputMode="adjustResize|stateVisible" as an OS-level second line of defense. Tried to verify live and hit a real limitation worth recording: this box's AVDs run -no-window (headless), and in that mode mInputShown never reports true via dumpsys input_method even for a manual, deliberate tap on the field -- confirmed by testing a plain tap directly, independent of any app code. The harness can't observe IME visibility here, so this fix is verified by code-level reasoning (LocalWindowInfo-driven focus is the standard, documented fix for this exact bug class) and confirmed window-focus DOES transfers correctly (mServedView moves to the bottom sheet's window), not by watching the keyboard actually appear. Real confirmation has to happen on-device. Also: SettingsActivity.saveAndFinish() only ever called DootWidget().updateAll() indirectly, as a side effect of RefreshWorker succeeding its network fetch -- so a slow or failing request could delay or block the widget from reflecting a setting the user just saved, even though every setting saved there (theme, text size, background, checkboxes) is already fully local and needs no network round trip to take effect. Now calls updateAll() directly and immediately after writing prefs; RefreshWorker still runs afterward to separately pull fresh server data. Verified installable and crash-free on a real API 36 emulator. Deployed as doot-widget.apk.
2026-08-06Widget: ship Material You theming, past-events restructuring, legibility ↵Peter Stone
fixes, new settings This lands the color-theming work that had sat uncommitted since a prior session (2026-07-28) -- every build published in between stripped it out deliberately to avoid shipping unreviewed work -- plus a full round of fixes and new features layered on top since it finally shipped: Theming (WidgetPalette.kt, new): - 5 themes now: NEUTRAL/ACCENT/TONAL (wallpaper-derived via Material You), VIVID (new -- all three text roles pull from a different accent slot instead of anchoring primary to neutral, for real hue variety), CLASSIC (fixed, wallpaper-independent). - Settings picker redesigned to match Android's native wallpaper "Basic colors" circular swatches (bottom half + two top quadrants, filled with each theme's actual buildWidgetPalette() output, not an approximation). - Per-source accent colors (colored checkboxes/bars) fully removed from the grid. Past events (DootWidget.kt, WidgetRows.kt): - Already-ended-today events pulled out of the hourly grid, shown as list rows above it instead (matching how past tasks already float) -- fixes the grid's start hour getting stretched backward by stale events. Legibility (WidgetRows.kt): - ShadowedText upgraded from a single-corner drop shadow to a 4-corner halo/outline (protects all sides of a glyph, not just one). - Halo color now tracks each palette's text luminance (dark halo for light text, light halo for dark text) -- a hardcoded black halo behind already-dark light-mode text was doing essentially nothing. Bumped opacity 0.45/0.55 -> 0.65/0.7 as the cheap, low-risk strength dial. New settings (SettingsActivity.kt, DataStore.kt): - Background transparency slider (0-85%, default 0% unchanged) -- exposed for testing per explicit request, not a default change. - Hide-checkboxes toggle: drops the leading checkbox/dot element entirely (not just hides the icon) so task titles land flush with event titles; tap-to-complete-from-widget trades off for TaskDetailActivity's Complete button. Also: today's moon phase in the TODAY header (moonPhaseEmoji, pure date computation, no network) -- verified against direct calculation before writing test assertions, not hand-computed. Removed the unused glance-material3 dependency (verified zero usages before removing; turned out to save ~2KB, not the ~280KB expected, since material3 itself already pulls the same transitive deps -- noted honestly rather than oversold). Every new pure-logic piece has unit tests (isPastEvent, moonPhaseEmoji, theme construction) -- 59 total, all green. Verified installable and crash-free via a real API 36 emulator launch before each publish, not assumed. Deployed as doot-widget.apk.
2026-08-06Widget: wrap the web dashboard in a WebView (DashboardActivity)Peter Stone
First cut per the 2026-08-06 feasibility check (verdict: easy, no architecture blockers): a plain WebView pointed at the configured server URL, cookie jar enabled for the existing session-cookie login (internal/auth/middleware.go's RequireAuth) -- no separate auth bridge needed, the widget's own bearer token is a completely different scheme and doesn't need to touch this at all. External links (e.g. a calendar event's source URL) escape to the user's real browser instead of getting stuck in the WebView. No deep-linking to specific tabs, no native-rendered chrome -- this is the minimal first step to validate the wrapped experience is worth building further, not the final shape.
2026-08-06Widget: add postpone (tomorrow/next week/next month) to task detail popupPeter Stone
Alongside Complete/Edit, doot-native tasks now get a Postpone button with a dropdown (Tomorrow / Next week / Next month), reusing the existing reschedule wiring (WidgetRepository.reschedule) the due-date picker already uses. Caught and documented a real divergence while writing the test: Java's LocalDate.plusMonths CLAMPS to the target month's last valid day (Jan 31 -> Feb 28), while the Go server's ComputeNextOccurrence (recurrence math) OVERFLOWS instead (Jan 31 + 1 month -> Mar 3) via time.AddDate. Verified by actually running both, not assumed -- a first draft of this test asserted the wrong (Go-style) behavior before checking. Not reconciled here, just accurately documented as a known inconsistency between this client-side helper and the server's date math. 4 new tests, all passing.
2026-08-06Fix Google Calendar TimeMin using server time instead of display timezonePeter Stone
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.
2026-08-06Widget: target Android 16 (API 36) natively, no compatibility fallbackPeter Stone
This is a single-user app, sideloaded onto exactly one phone, never distributed broadly -- "what's the widest range of devices this should install on" was the wrong question to ask about it. A prior pass (2026-08-05) walked minSdk down to 31 on that reasoning, without checking what device this actually runs on: Android 16 / API 36. minSdk=targetSdk=compileSdk=36 now, matching the real device exactly. Verified both directions on real emulator targets, not assumed: installs and launches cleanly (no crash, target_sdk_version=36 confirmed via logcat) on a new dedicated API 36 AVD (doot_test_api36), and correctly refuses to install on the existing API 34 emulator (INSTALL_FAILED_OLDER_SDK) -- proving the constraint is real, not just declared. AGP 8.2.0's bundled D8 still warns "API level of 36 is not supported by this compiler" even with build-tools;36.0.0 installed locally (AGP doesn't delegate to the standalone SDK build-tools binary) -- confirmed harmless via the install+launch verification above, not chased further since a real fix means upgrading AGP itself, a bigger change than justified here. Audited the rest of the app for SDK-version fallback/polyfill code that could now be simplified given the guaranteed floor: found none: the only place minSdk mattered was this file.
2026-08-05Widget: fix "complete works once, then stops" race between overlapping ↵Peter Stone
completions Root cause traced from server logs, not guessed: every completion request was succeeding server-side (100% 200s, including a 5-tap burst spanning different tasks), and each successful completion's response payload was correctly shrinking. So the failure wasn't dispatch or network -- it was that a successful completion could still get silently undone client-side. fetchAndPersist does an unconditional full overwrite of the cached item list on every successful GET. CompleteWorker/DeferWorker run one instance per task id with no ordering guarantee between different ids' workers (different unique work names, no KEEP protection across them -- that protection only ever covered same-task double-taps). So: tapping complete on task A starts a GET that's still in flight; tapping complete on task B before A's GET returns optimistically removes B locally; A's slower GET response, captured before B's completion landed, then overwrites the cache and silently resurrects B. Fix: track locally-optimistic removals with a timestamp (PendingRemovals.kt) and filter them out of every fetchAndPersist write for a bounded TTL (2 min), regardless of which worker's fetch is doing the writing. The TTL means a completion that never actually confirms (permanent network failure) still self-heals via the next periodic refresh, matching an existing self-healing property already relied on elsewhere in this codebase, instead of hiding the task forever. Added PendingRemovalsTest.kt (pure-function unit tests, no Android runtime needed) covering the exact race scenario plus TTL expiry and edge cases. Verified the tests actually catch a regression by deliberately reverting the fix to a no-op against a real backup, confirming 3 tests failed with the exact expected assertion, then restoring and confirming green again. Built, tested, and published as doot-widget.apk.
2026-08-04Widget quick-add: surface errors instead of failing silentlyPeter Stone
addTask()'s failure path had no .onFailure handler, so a failed POST (network hiccup, server down) left the Add sheet just sitting there with zero feedback -- looked like the button did nothing. Also: the five calls chained after a successful addTask (updateTask, reschedule, setTaskProject, setTaskLabels, setTaskRecurrence) were still unchecked, so a task could get created with silently-dropped metadata even with the addTask fix in place. Both paths now collect and toast what failed; the task itself still gets created and the sheet still closes, since aborting would leave a task that already exists server-side with no way to tell the user without a duplicate.
2026-08-04Fix production wedge: propagate context to Google Calendar API callsPeter Stone
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.
2026-07-28fix: Google Tasks due dates shifted a day earlier in negative-offset timezonesPeter Stone
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.
2026-07-25fix: undated tasks were landing in the widget's scheduled-events gridPeter Stone
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).
2026-07-18fix(widget): wrap TomorrowSection in a Column to stop rows overlappingPeter Stone
TomorrowSection emitted its divider, header, and every event/task row as top-level siblings with no shared layout container. Since the whole function is composed inside a single LazyColumn item{} slot (one RemoteViews node), nothing told Glance to stack them vertically -- they all rendered on top of each other instead of flowing top-to-bottom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-18Add bucket CRUD and read-only Projects/Labels to Settings pagePeter Stone
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
2026-07-18Rework Tasks tab: Chains section, checklist modal, project visibilityPeter Stone
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
2026-07-17Extend chain creation to accept per-task description and priorityPeter Stone
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
2026-07-17Document deferred UI screens for chains and bucketsPeter Stone
Fleshes out the "not built" notes in both specs into actionable follow-up scope: what data/API layer already exists, what the screen needs to do, and precedent to follow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-17Guard deploy script to only run on tungstenPeter Stone
Refuses to run if hostname doesn't match the documented deploy host, preventing an accidental deploy from a sandbox/dev environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-17Implement linear task chains and recurring maintenance bucketsPeter Stone
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
2026-07-17chore(deploy): remove abandoned push-to-deploy scaffoldingPeter Stone
deployment/deploy duplicated scripts/deploy (the one actually in use). deployment/post-receive documented installing a git hook at /site/doot.terst.org/app-code/hooks/post-receive, but that hook was never actually installed and the app-code checkout it depended on has been removed -- this path was never wired up.
2026-07-17fix(widget): autofocus quick-add title field, redesign as blank edit formPeter Stone
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.
2026-07-16Add implementation plan for task budgets and availabilityPeter Stone
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQaPGQVSfmKUiHXB87qRTC
2026-07-16Show scheduled/available badge on widget TODAY headerPeter Stone
Threads the /api/widget budget_status field through the Android widget's DataStore-backed cache (a new BUDGET_STATUS_JSON pref, since the widget decomposes WidgetResponse into individual prefs rather than caching it whole) and renders a small "Nm/Nm" badge next to TODAY when there's a nonzero tracked load.
2026-07-16Show a scheduled-vs-available indicator on the web timeline's Today sectionPeter Stone
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
2026-07-16Suggest an estimate from same-project/label averages on task detailPeter Stone
2026-07-16Add availability CRUD, task estimate, and budget-tracked toggle endpointsPeter Stone
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
2026-07-16Surface budget_status on GET /api/widget when tracked tasks existPeter Stone
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
2026-07-16Fix overlapping-event double-subtraction in ComputeBudgetPeriod; clarify ↵Peter Stone
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
2026-07-16Add pure availability-minus-events and scheduled-load computationPeter Stone
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.
2026-07-16Infer a default estimate from same-project/label averagesPeter Stone
2026-07-16Add budget-tracked toggles for projects/labels; fix SetLabelColor wiping the ↵Peter Stone
flag
2026-07-16Add availability_blocks CRUD to the store layerPeter Stone
2026-07-16Add estimated_minutes to native task read/write/recurrence pathsPeter Stone
2026-07-16gofmt internal/models/types.go and budget.goPeter Stone
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
2026-07-16Add budget/availability model typesPeter Stone
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
2026-07-16Add schema for task budgets and availability blocksPeter Stone
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
2026-07-16fix(agent): pass project_id through on Agent API task creationPeter Stone
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
2026-07-16Update worklog for task labels and projects featurePeter Stone
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
2026-07-16feat(widget): show project color as a small accent on task rowsPeter Stone
2026-07-16feat(widget): add project picker and label editor to task detail popupPeter Stone
2026-07-16feat(widget): add Android data layer for projects and label colorsPeter Stone
2026-07-16test(tasks): add tests for project color threadingPeter Stone
Tests verify ProjectColor is populated in BuildTimeline and copied to WidgetItem.
2026-07-16feat(tasks): thread project color from BuildTimeline to WidgetItemPeter Stone
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.
2026-07-16test(tasks): add test cases for project and label endpointsPeter Stone
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
2026-07-16feat(tasks): add HTTP endpoints for projects and label colorsPeter Stone
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
2026-07-16feat(tasks): add label color CRUD store methodsPeter Stone
2026-07-16feat(tasks): add Projects CRUD store methodsPeter Stone
2026-07-16feat(tasks): add projects/labels schema, wire project_id through native tasksPeter Stone
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.
2026-07-16Add implementation plan for task labels and projectsPeter Stone
Nine tasks: schema/model + project_id wiring, Projects CRUD, label-color CRUD, HTTP endpoints, TimelineItem/WidgetItem project-color threading, Android data layer, task-detail popup UI (project picker + label editor), widget grid color accent, build/deploy.