a local-first atmospheric chalky task planner

Commits on main

committed
committed
committed
deps: atproto Spaces alpha SDK moves to the 2026-10-22 build (0.0.0-spaces-alpha-20261022141228, published 13 min after atproto ac0c9f1, the next pds-spaces-alpha image), and space reads accept either PDS build's commit shape so this client deploys ahead of the PDS flip — ac0c9f1 replaced `#signedCommit` {ver,hash,ikm,sig,mac,rev} with `#commit` {ver,repo,space,rev,data}, and the SDK validates every response, so the 10-15 SDK rejects the new PDS's getLatestCommit and any listRepoOps page that reaches the head (every sync cycle), and the 10-22 SDK rejects the old PDS's the same way. repo.ts's commitShapeMismatch takes a body rejected at Output/commit as-is: head() reads commit.rev (both shapes carry it), listOps() drops the unused commit and re-validates the rest, so a page wrong anywhere else still throws. Drop it once bag.laugh.town runs ac0c9f1. The rest of the range doesn't touch chalky: commit MACs only apply to space-credential / Atproto-Space-Want-Mac reads (OAuth reads skip them), getRepo (CAR→STAR, excludeValues gone) and getSpaceCredential (auth in body) aren't called, and the only DID-doc entry read is #atproto_pds, not #atproto_space. oauth-client-browser's dist is byte-identical, so the WebKit null-IDB patch carries over (renamed; same patch_hash). Verified by driving SpaceRepo through Vite SSR against mocked responses: main fails head/listOps on the new shape, the bump alone fails on the old one, bump + fallback passes on both and still rejects a page with invalid ops; not exercised signed-in against a real PDS; pnpm check 0 errors 0 warnings, pnpm build ok, Dockerfile build ok spaces-alpha-20261022
committed
app: the carry-over sweep waits for a pull made today, so a tab that went days without syncing no longer moves finished tasks onto today — Mac Safari (device jjwpyk) last synced 10-18 07:19Z, sat hidden for two days (hidden tabs skip cycles), and on 10-20 the rollover check swept its stale copy at 20:30Z with no PDS traffic: the engine's once-per-session `reconciled` was still true from the first cycle. When it reloaded at 02:18Z its 16 records went out via conflict-path putRecords; their fresh place stamps beat the remote's in the field merge, so 10 tasks completed elsewhere landed on 10-20 still checked and 2 deleted ones were recreated (reconstructed from the PDS request log and space_record/space_record_oplog, no device trail). settings now keys reconciliation by day (`reconciledFor`, the day a successful cycle started), the engine reports every successful cycle rather than the first, and a never-synced device keeps sweeping locally (`markLocalOnly`). The failure-limit fallback no longer releases the sweep on a device holding synced records (`reconcile.heldForPull`); a held sweep logs `sweep.carryOver.deferred` once per day. sync-triage skill documents the signature and the new events. Verified in the preview: a 10-18 task with a cid stayed put after a cycle dated yesterday and with localOnly set, and moved to today after a cycle dated today; not exercised signed-in or across a real hidden-tab rollover; the 16 records on the PDS are not repaired; pnpm check 0 errors 0 warnings, pnpm build ok
committed
committed
app: drag-to-reorder no longer gets worse the longer a drag goes on, plus a dependency sweep — svelte-dnd-action 0.9.74's getBoundingRectNoTransforms read rect.x/rect.y off getVisibleRectRecursive's result, which has neither, so any row measured mid-flip came back NaN; the shadow-rect cache (cleared only on scroll or drop) then held a NaN for that index and its hysteresis check pinned the placeholder whenever the pointer reached that row, one more row per flip the observer happened to catch. Upgraded to 0.9.80 (upstream reads rect.left/top) and re-ported both chalky patch hunks (no synthetic click on touchend with delayTouchStart; no preventDefault on touch handle starts). Also: svelte 5.57.2, vite 8.3.4, vite-plugin-svelte 7.3.1, svelte-check 4.7.7, bits-ui 2.19.5, @lucide/svelte 1.59.0, milkdown crepe+kit 7.22.3, marked 18.1.1, idb 8.0.4, typescript 6.0.3, @types/node 24.19.2, hueful 0.8.1, satori 0.62.0. Satori now lays out with Taffy (layout.wasm replaces yoga.wasm) and defaults to content-box, which pushed the share cards' padding outside every fixed width (values off the right edge, footer cut, tiles two per row) — the card helper sets boxSizing border-box; THIRD_PARTY_LICENSES swaps yoga-layout for taffy. Held back: TypeScript 7 (svelte-check refuses it), @types/node 26 (runtime is Node 22), the atproto spaces-alpha SDK (must match the PDS build; separate task). Verified in the visible preview: a scripted 25s drag left 6 of 7 targets on the wrong row before and 0 after; both share cards render 1080x1350 and look right; hueful's normalizeHex/inkFor outputs unchanged; notes editor renders; pnpm check 0 errors 0 warnings; pnpm build ok (main chunk +17KB vs live); touch not exercised on a device
committed
app: a Stats page in the account menu, shareable as two images, and a setting to hide it — stats.ts computes usage from the local store (tasks created/done, current and best daily streak, active days, busiest day, median created→done and same-day share, peak hour, done per weekday, created/done per month, color counts, still open / in idk when, moved, notes, subtasks, recurring series and occurrence hits; tombstones skipped, local time). StatsModal shows it as tiles plus bar rows (palette-colored for colors) and is wired like Credits: ui.statsOpen, ?panel=stats, Escape. Share renders two 1080×1350 cards (headline tiles + odds and ends; months, colors, weekdays), both titled "chalky.town stats", with Satori in the chalkboard palette, pre-rendered when the modal opens so the tap hands them to navigator.share inside Safari's user activation; where files can't be shared (or the share is refused) an in-app menu previews both with Copy and Save per card and a Post to Bluesky draft — never a silent download (the first version downloaded two files and browsers dropped the second, which is why the shared image looked empty). Satori is pinned to 0.33.5 (the four releases from the last day skipped) via its standalone build, initialized with yoga.wasm by URL; harfbuzzjs fetches /hb.wasm from the page directory, so a small vite plugin serves it in dev and emits it in the build; Gluten comes from @fontsource/gluten 5.3.0's woff files since Satori can't read woff2. All of it lazy-loads on the first Stats open. Settings gains "Hide the Stats page" (settings.hideStats, synced through town.chalky.config's new hideStats field, lexicon updated but not yet republished), which drops the menu item and ignores ?panel=stats. THIRD_PARTY_LICENSES lists satori (MPL-2.0), yoga-layout and harfbuzzjs. Verified: page numbers match an independent count of the PDS space records; both cards checked visually; iOS 26 simulator Safari opens the native share sheet with the PNG; the preview pane falls back to the menu; hide toggles both ways; pnpm check 0 errors 0 warnings; pnpm build ok
committed
app: the phone new-task sheet works again after the keyboard closes, and the notes field loads without the jumble — 91fb7a3's visualViewport sync read kbInset right after writing it inside its own effect, so with the keyboard up the effect depended on its output and looped (cleanup zeroed it, sync set it back) until Svelte threw effect_update_depth_exceeded, leaving the sheet frozen at its keyboard-up bottom/max-height and the UI wedged so the swatches and X stopped responding; sync now only writes. The sheet also scrolled sideways: [data-tip] bubbles are invisible on touch but still laid out, and the new + sat at the edge, so its tooltip widened the card ~23px — tooltips now aren't rendered under (hover: none), and the + tip is right-aligned for desktop; separately WebKit counts the card's end padding past the header/title negative-margin bleed as overflow (~18px), so the card is overflow-x: hidden. The notes loading state was a centred grid holding "Loading editor…" plus Crepe's half-mounted surface (placeholder one word per line); it's now a stand-in at the editor's finished size — an empty toolbar row and the note text or placeholder — with Crepe mounting into a hidden surface. Verified in the iPhone 16 Pro simulator over the Web Inspector protocol: faked keyboard open/close via visualViewport lifts the sheet to bottom 318px then back to the edge with no errors, card scrollWidth no longer exceeds its width, swatch tap selects salmon, X closes; loading stand-in screenshotted under a temporary 6s delay (removed); pnpm check 0 errors 0 warnings
committed
app: past completions stay put, series get an ending, and undo/redo joins the header — toggleDone's pull-to-today now fires only off a FUTURE day or Someday (done && day > today || day === null): checking off a task on a past day leaves it there as a record of when it happened, instead of dragging history forward. A new endRecurrence caps a series at today — until = today (keeping an earlier cap), count dropped as superseded — so every past occurrence record (completed, skipped, or untouched projection) keeps rendering exactly as it did while tomorrow onward stops; distinct from the X (remove repeat, orphans the overrides) and Delete (removes everything), and undoable like both since it rides setRecurrence's record() transaction. The details card offers it as 'End repeating' only when there's something to end (series started, doesn't already stop by today), sharing one footer row with Delete task — End flush-left, Delete pinned right via margin-left:auto so it holds the corner when End isn't offered. The header grows Undo/Redo buttons beside the account menu, mirroring ⌘Z/⌘⇧Z for mouse and touch where those don't exist — disabled (dimmed, not hidden, so the cluster never shifts) off canUndo/canRedo, with the app's coarse-pointer hit extenders. Verified in the preview: past/today completions stay, future/Someday move; ending a daily series kept yesterday's completed occurrence and today, dropped tomorrow, summary reads 'until …'; the header Undo restored the recurrence and lit Redo
committed