<feed xmlns='http://www.w3.org/2005/Atom'>
<title>doot.git/web/templates/partials, 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-09-20T08:11:38+00:00</updated>
<entry>
<title>Make shopping items editable: tap to rename, move store/section, delete</title>
<updated>2026-09-20T08:11:38+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-20T08:11:38+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=c7b18e70ea87c287240243aed867b3a8b51689cb'/>
<id>urn:sha1:c7b18e70ea87c287240243aed867b3a8b51689cb</id>
<content type='text'>
Tapping an item outside shopping mode now opens an edit sheet. User-added items
can be renamed, moved between stores and sections, and deleted. PlanToEat items
show why they cannot be edited — the client is read-only, so presenting fields
that silently would not save is worse than saying so — and offer removal, which
marks them checked, the same mechanism shopping mode already uses.

This needed a store registry first. Stores previously existed only because some
item referenced them by name, with Trello's list names quietly acting as the
registry; removing Trello as a source made the gap visible. Without a registry
you cannot move an item into an empty store, and moving the last item out of a
store makes it vanish mid-edit. Migration 034 adds shopping_stores and
shopping_sections, seeded from the stores currently in use, and gives items a
section column — sections previously existed only as PlanToEat category
strings, so a user item could not be filed under one at all.

Registered stores and sections now appear in the list even when empty, which is
the whole point of the registry.

Two deliberate choices about destructive edges:
  * Renaming a store renames it and moves its items in one transaction. Items
    reference the store by name, so doing either alone orphans them.
  * Deleting a store or section keeps the items, moving them to Unassigned or
    clearing the section. Losing the list is far worse than re-filing an item.
  * An UPDATE matching no rows is an error, not a silent success, so a stale id
    cannot look like a saved edit.

Section options are rebuilt per store in the edit sheet rather than shipped as
one flat list, so a Costco item cannot be filed under a Safeway aisle.

18 new store tests, 11 handler tests.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Add .ics calendar feed support</title>
<updated>2026-09-20T07:57:36+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-20T07:57:36+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=4d160762aeef531208f8cf6094a0e58a9e7d53ec'/>
<id>urn:sha1:4d160762aeef531208f8cf6094a0e58a9e7d53ec</id>
<content type='text'>
doot reads Google Calendar through a service account, by calendar ID. That
cannot consume an .ics URL, and it cannot see a calendar subscribed to inside a
personal Google account either, because Google does not allow re-sharing a
subscribed calendar. Published schedules — sports fixtures, school terms, bin
collections — are distributed almost exclusively as .ics, so none of them could
be added at all.

Feeds are now fetched on the normal calendar refresh and merged into
calendar_events alongside Google events, so the timeline, widget and everything
downstream keep seeing one list and needed no changes.

Failure handling is deliberately asymmetric. A Google error is still returned as
an error, unchanged, so an outage there surfaces. A single failing feed is
recorded against that feed and skipped, because one dead URL should not take the
whole calendar down — and last_error is shown in settings, since a feed that has
quietly stopped resolving otherwise looks identical to a team with no games
left.

The parser handles what real feeds actually contain: line unfolding, CRLF and
LF, UTC / TZID / VALUE=DATE times, and escaped text. Two details worth naming:
a TZID time is wall-clock in that zone, and reading it as UTC would put a 1:15pm
Pacific kickoff on the timeline at 1:15pm local — wrong by the offset but
entirely plausible-looking. And non-calendar content is a hard error rather than
zero events, because an HTML error page from a moved feed URL is the common
failure and "no events" reads as success.

RRULE is deliberately unsupported: every schedule feed this targets expands
recurrence itself, one VEVENT per fixture, and a correct expansion is a large
amount of subtle code. A recurring event appears once rather than wrongly.

Settings gains an add/enable/disable/remove UI. webcal:// links are accepted and
rewritten, since that is how feeds are usually advertised.

16 parser tests, 5 store tests. Verified against the real Seattle Seahawks feed:
21 events, all with valid times.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Drop Trello as a shopping source, importing the two groups worth keeping</title>
<updated>2026-09-20T07:47:48+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-20T07:47:48+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=c5ab2f9a7f6a0b1e456e13934268d1509504c55a'/>
<id>urn:sha1:c5ab2f9a7f6a0b1e456e13934268d1509504c55a</id>
<content type='text'>
The Trello "Shopping" board had become a dumping ground: of 24 cards, 13 had no
list at all (rendering as "Uncategorized"), and the rest were spread across
Costco, Hardware Store, "Perennial " (trailing space, so a separate store from
"Perennial") and online. Its items were read-only in doot — no rename, no move,
no delete, only hide — and its list names were doubling as an accidental store
registry, which is the only reason stores like Hardware Store existed at all.

Migration 032 imports the two groups asked for, Hardware Store and the unlisted
cards, into user_shopping_items. Costco, Perennial and online cards are left
behind. Nothing is destroyed: the cards remain in Trello, this only stops doot
displaying them. Imported rows become ordinary user items, so they also become
editable, which Trello-sourced items never were.

Trello remains the source for the other eight boards; this is only the Shopping
board feeding the shopping list.

Also updated the quick-action modal's Shopping tab, which posted source=trello
with a Trello list_id. Left alone it would have kept creating cards that no
longer appear anywhere. It now posts source=shopping with a store name, handled
by a new case in HandleUnifiedAdd, and its picker reads from a new
/partials/shopping-stores endpoint. The Trello-lists endpoint it used to call
is deleted along with its route.

Note for the edit feature still to come: stores have no existence independent
of items, so a store with nothing in it cannot be chosen. That was previously
masked by Trello's lists persisting. "Move between stores and sections" needs a
real stores/sections table.

Migration tested against a seeded snapshot of the real board, including the
trailing-space store name, other boards, replay safety and the empty-cache
no-op. Two existing tests asserting Trello check-state in shopping mode were
updated to plantoeat, the only remaining external source.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Stop per-store add forms jumping to the bottom of the page</title>
<updated>2026-09-20T01:32:44+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-20T01:32:44+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=b970be11ee895c34e019231022e9e5244e4aec2f'/>
<id>urn:sha1:b970be11ee895c34e019231022e9e5244e4aec2f</id>
<content type='text'>
The inline "Add to &lt;store&gt;..." forms submitted with a bare
hx-swap="innerHTML" against #shopping-items-list. Those forms live *inside*
that target, so the swap destroys the form that submitted it and the page ends
up scrolled to the bottom rather than staying on the store being added to. The
quick-add form at the top never had this because it sits outside the target and
already carried an explicit scroll: modifier.

Each store section now has a stable id and its inline form scrolls back to it,
so adding to a store keeps that store in view.

Adds a `slug` template function for the id: store names are user data
("Trader Joe's", "Hardware Store") going straight into element ids, so anything
outside [a-z0-9-] is collapsed to a dash and an empty result becomes "unnamed"
rather than an invalid, shared anchor.

Tests render the real template with the real func map, and assert every inline
add form has a scroll target — the bug is trivially reintroduced by copying a
section.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Make the day's list a pull of committed work, not a push of everything unblocked</title>
<updated>2026-09-03T02:34:25+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-03T02:34:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=1477ee531045388de9e7e797200e5cd14e4a6d0e'/>
<id>urn:sha1:1477ee531045388de9e7e797200e5cd14e4a6d0e</id>
<content type='text'>
due_date had been carrying two incompatible meanings: "this has a real
deadline" (Bills, meds, a signup cutoff) and "this is when I intend to
work on it." Chains leaned entirely on the second -- every step was
stamped due_date = now the moment it unlocked -- so a five-minute call
and a six-month certification both arrived looking overdue, and the only
way to quiet them was to hand-reschedule. The list therefore always
contained work that could not be finished that day, which made ending a
day at zero impossible by construction rather than by effort.

Splits the two apart:
  - due_date keeps only its real meaning: an externally-imposed deadline.
  - committed_date (new, migration 031) means "I chose this for today."

Chain steps no longer get a due date on unlock (CreateChain, advanceChain,
and the delete-promotion path), and are excluded from the undated list so
they don't simply fall through to it instead. They reach the day only by
being committed, via a "+ today" control on the chain card. Bucket-active
items get the same control. BuildTimeline pulls in committed tasks,
deduped against tasks already present for a different reason (committed
and due-today are two reasons for one row, not two rows).

Migration 031 also clears the auto-stamped due dates from open chain
steps; completed steps keep theirs as historical record.

Two supporting changes made while in here:
  - Extracted the native_tasks SELECT list into nativeTaskColumns. It had
    been copy-pasted into nine queries that must each agree exactly with
    scanNativeTasks' field order; adding committed_date meant editing all
    nine correctly or silently breaking whichever was missed.
  - Extracted templateFuncMap() and added TestTemplates_AllParse. New()
    only logs a warning when template parsing fails, so a malformed
    template shipped silently and surfaced as a broken page at runtime.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Fix swipe-nav false positives on scroll, shopping quick-add overflow</title>
<updated>2026-08-30T20:46:36+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-30T20:46:36+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=a17a15966be5db6de423cdcb7c427195102b99d8'/>
<id>urn:sha1:a17a15966be5db6de423cdcb7c427195102b99d8</id>
<content type='text'>
Swipe-to-change-tab only ever compared horizontal touch delta against
a 50px threshold, with no check against vertical movement -- an
ordinary scroll through a long tab (the shopping list, in particular)
drifts that far horizontally from natural thumb motion alone, and was
getting misread as an intentional swipe. Since timeline sits right
before shopping in TAB_ORDER, that's specifically what a rightward
drift landed on: "shopping randomly jumps back to timeline." Now
requires the horizontal delta to be at least 2x the vertical delta
before treating it as a swipe instead of a scroll.

Also fixed the shopping tab's inline quick-add row: the store &lt;select&gt;
had no width cap, so a long store name could claim enough width to
push the Add button past the right edge of the screen -- not just
cramped, actually unreachable. Capped the select's width and let the
name input actually shrink (needs min-w-0 alongside flex-1, since flex
items don't shrink below their content size by default), with
flex-wrap as a backstop for anything that still doesn't fit.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<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>Update conditions page: refresh V1cam URL, zoom out Windy maps</title>
<updated>2026-08-23T17:40:21+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-23T17:40:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/doot.git/commit/?id=18783b813af946e1e940bfeb1a0bdc1e7ce9f95f'/>
<id>urn:sha1:18783b813af946e1e940bfeb1a0bdc1e7ce9f95f</id>
<content type='text'>
V1cam's YouTube video ID had gone stale; resolved the current one via
USGS's url.usgs.gov/v1cam shortener. V2cam was already current. Zoomed
the Windy embeds/links out 1-2 levels on both the standalone conditions
page and the in-app conditions tab.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
</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>
</feed>
