fix: order collisions, task-complete undo, filing from a fresh install, and four defects - #10
Merged
Conversation
create() set `order = siblings.length`, but a delete leaves a hole, so the count can equal an order still in use: create three tasks (0, 1, 2), delete the middle one, and the next create takes order 2 again. Two tasks sharing an order make swapOrder a SILENT no-op — swapping two equal values changes nothing — so the "Move down" menu item appears and does nothing. It also makes the sort order between the colliding pair arbitrary, because both call sites sort by `order` from different sources and a stable sort then preserves two different input orders. Use max(order)+1, which is what moveToSection already does, with a comment there explaining exactly this hazard. Two regression tests: one pins create()'s gap-robustness, one drives a real reorder across a delete-induced gap and asserts the list actually changes.
Every toast rendered an "Undo" button, including the ones that pass no onAction (onSaveRecurrence, onRemoveRecurrence). Clicking it merely dismissed the toast, so the user was told a reversal had happened when nothing had been reversed — worse than showing no button at all. Render the button only when onAction is supplied, and drop the now-dead `if (onAction)` guard inside the handler.
Completing a plain task wrote straight to the model with no feedback and no way back — only recurring tasks got a toast. An accidental tap on a 16px checkbox silently removed the task from view. Show a "Task completed" toast with Undo, in the completing direction only: un-checking a task IS the undo, so a toast there would offer to redo what the user just reversed. Undo sets completed:false explicitly rather than calling toggleCompleted a second time. The user can un-check the box by hand while the toast is still up, and a second toggle would then re-complete the task instead of undoing anything.
Same defect just fixed in tasks.create: both assigned `order` from the sibling COUNT, and a delete leaves a hole, so the count can equal an order still in use. Both models expose swapOrder and drive a Move up/down menu, and swapping two equal order values is a silent no-op. Use max(order)+1 in both. One regression test each, asserting the orders stay distinct across a delete-induced gap.
"Move to…" was gated on there being another section to move to, so a fresh install — one area, one section — never saw the item at all. There was no route from a task to somewhere else, and nothing hinting one existed. The File shortcut on the notepad was gated the same way and so never appeared either. Add "+ New section…" to the move picker and drop the gate from both. The new section is created in the task's own area, so the user is never asked to pick one, and the move then goes through the ordinary path — same toast, same cascade-race swallows. Undo removes the created section as well as moving the task back. The section exists only because of this action, so leaving an empty stray would make undo a partial reversal. It is removed only after the task has moved out, so the delete can never take a task with it.
All three share a shape: read state, await a write, act on the value read before the await. Precedent for the fix is handleToggleComplete's `completing` Set. - capture commit(): the input is deliberately not cleared until the write resolves, so a second Enter mid-write read the same text and created a duplicate task. Guarded with a boolean. - the section "Add task" row: identical, guarded with a Set keyed by section so two different rows can still commit concurrently. - onCycleTheme: read currentChoice, which applyState only updates after the round-trip, so two fast clicks computed the same next choice and the cycle skipped a step. Advances an optimistic cursor instead, re-synced to the model in applyTheme so a system flip cannot leave it stale. Verified in the browser: two same-tick clicks on the theme control now advance two steps (system → dark).
…levels Two defects surfaced by the Plan 3 reviews. Alignment: `.next-card` carried its own 16px inline padding, which nested inside `.task`'s, so the hero row sat 16px in from both edges — its title and its right-hand columns were 16px off every group row below. §7 asks for alignment "across every group" and the hero is not a group, so no check covered it. The card now pads vertically only and the "Next" label supplies its own inset. Measured at 375px and 1280px: left, right, title and star edges all match the group rows exactly. Heading levels: group headings were <h3> under the page <h1>. On the Today tab the Next card's <h2> bridged the gap, but the other three tabs have no Next card, so the outline skipped a level. The Next card and the groups are siblings in the same panel, so the groups belong at <h2> too.
onSaveRecurrence and onRemoveRecurrence close the dialog with rerender:false, which sets a pending-focus flag and leaves the write's notify to consume it. On the /not found/ swallow there is no write, so no notify and no render, and the flag survives. That used to be harmless. With the focus fallback added in Plan 3 it is not: the flag is consumed by the next 60s tick instead, pulling focus out of whatever the user had moved on to up to a minute later. Render explicitly on both swallow paths so the flag is spent immediately.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eight commits. Every change is either a defect confirmed by reproduction or a fix you asked for directly.
The "can move up but not down" bug — confirmed, root cause found
create()setorder = siblings.length. A delete leaves a hole, so the count can equal an order still in use: create three tasks (0, 1, 2), delete the middle one, and the next create takes order 2 again.Two tasks sharing an order make
swapOrdera silent no-op — swapping two equal values changes nothing — so "Move down" appears in the menu and does nothing. It also makes the sort order between the pair arbitrary, because the view and the controller sort from different sources.Proven with a failing test before the fix:
d.orderafter a delete-induced gap2, colliding withc.orderswapOrder(c, d)moveToSectionalready usedmax(order)+1with a comment explaining this exact hazard.createnow does the same — in all three models: tasks, sections and areas all had it, and all three drive a Move up/down.Note: you said moving works on your phone. The defect is real regardless — it only bites once a delete has left a gap, so a database that has never hit that state behaves fine.
Undo when a task is checked off
Completing a plain task wrote straight to the model with no feedback and no way back; only recurring tasks got a toast. Now shows "Task completed" with Undo, in the completing direction only — un-checking a task is the undo.
Undo sets
completed:falseexplicitly rather than toggling again, because you can un-check the box by hand while the toast is still up and a second toggle would re-complete it.Filing a task from the Focus tab
Not a bug — a design gap. "Move to…" was gated on
sections.length > 1, so a fresh install (one area, one section) never saw the item at all, and the notepad's File shortcut was gated the same way. There was no route from a task to anywhere else and no hint one existed.The picker now carries "+ New section…", and both gates are gone. The new section is created in the task's own area, so you are never asked to pick one. Undo removes the created section as well as moving the task back, so it is a complete reversal rather than a partial one.
Four more defects from the backlog
onAction, so clicking it merely dismissed and told you a reversal had happened when none had.onCycleThemeread a value the write only updates afterwards, so two fast clicks skipped a cycle step..next-card's own inline padding nested inside.task's, putting the hero row 16px in from both edges relative to every group row.<h3>under the page<h1>; the Next card's<h2>bridged it on Today, but the other three tabs had no bridge.Verified
biome check .clean,npm run buildclean, no console errorscompleted 0 → 1, row disappears, toast; Undo →completed 0, row returns1 section → create → 2 sections + task moved → Undo → 1 section + task backNot covered
A real-device pass is still owed and I can't do it — the in-app browser is desktop Chromium at a resized viewport. Worth noting that a CDP click landing dead centre on the 16px checkbox did not fire the handler, while a scripted click did; that's the same tool limitation already recorded for Enter, not an app bug, but it means touch behaviour here is unverified.