You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Fix ten usability problems found in an end-user walkthrough - #93
A dismissable ErrorBanner component, used by all three pages
What the review rounds changed
The rollback in #82 exposed a subtler problem: a rejected bulk write may still have partly landed, so the restored list is a guess until a read confirms it. That took several rounds to get right, and the result is:
the write failure is stored with the book it happened in, and the banner is always derived from it - never rebuilt from itself, so repeated failed refreshes replace their sentence instead of stacking;
a successful read of the same book retracts the "could not be verified" part and keeps the failure itself, because a read confirms rather than refutes that the write was rejected;
a manual refresh no longer clears it, a book switch drops it along with its banner, and dismissing it is final.
Also from review: Matrix cards are no longer role="button" containers wrapping their own status buttons, and the board's date-window note is exposed to assistive tech instead of living in a hover-only title on an aria-hidden span.
Verification
171 tests pass across 32 suites, 8 of the suites new on this branch (optimisticUpdate, useModalDialog, statusTimestamps, bumpNotice, hiddenSummary, reconcileMessage, plus additions to dateUtils)
tsc --noEmit clean, npm run build clean, lint unchanged from main
Each fix was checked live in a signed-in browser, including forcing writes to fail by request interception to exercise the rollback and banner paths
Reviewed by two models over nine rounds, ending in two consecutive clean rounds from both
Failed writes left the board showing state that was never persisted: the
only recovery was a deferred refetch, which cannot succeed in exactly the
case that caused the failure (offline). Snapshot the affected rows before
the optimistic update and restore them when the write is rejected, guarded
by the existing mutation version and book id so a rollback can never
clobber a newer edit.
Covers Matrix drag/status/field/tag mutations, the Scrum equivalents, and
the Cancelled bulk delete.
Closes#82
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The top bar was a non-wrapping flex row with a flex-growing spacer, so on a
390px viewport the Add TODO and Refresh buttons were laid out past the right
edge. The page does not scroll horizontally, which made them unreachable.
Below 640px the bar now wraps, the spacer is dropped, and the book switcher
is capped so it cannot push the actions off-screen.
Closes#83
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The add and detail overlays were plain divs: no role, no accessible name,
and focus stayed on the page behind them, so a screen reader or keyboard
user was never told a dialog had opened and could tab straight out of it.
Both now expose role=dialog with aria-modal and a name, take focus on open,
trap Tab, and return focus to whatever opened them. Matrix cards also gain
keyboard activation, which they lacked entirely.
Closes#84
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The same instant was spelled four ways: a short absolute date on Matrix
cards, a relative one on Scrum cards, a locale numeric date on Cancelled,
and a medium date-time in the detail view. One card even read
'Planned: Sep 30 -> tomorrow', mixing two scales inside a single range.
Inline dates now all go through formatRelativeDate, and every absolute
rendering - tooltips and the detail view alike - goes through a single
formatAbsoluteDateTime helper.
Closes#86
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Two problems in one place. Stale non-terminal tasks have their dates moved
forward automatically, but nothing said so, and the pre-bump plan was never
shown - so the card presented an app-invented date as the user's plan.
Separately, sending a task back to New or reopening a finished one cleared
its lifecycle timestamps on screen but never persisted the clear: the
payload used undefined, which the stores read as 'no change'. The stale
Started/Finished value came back on the next refresh, which is what made a
never-started task look like it had begun.
Cards and the detail overlay now show the original dates under an
'Originally planned' label with a tooltip explaining the move, and the
cleared timestamps are sent as explicit nulls.
Closes#85
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The visible result count and the delayed live region carry the same
sentence, so a screen reader read it once as static text and again as an
announcement. The visible copy is now hidden from assistive technology.
Closes#87
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The error banner was rendered inline on each board page with no way to
close it, so a transient failure stayed on screen until the next
navigation. Extract a shared ErrorBanner component with a dismiss
button and wire Matrix, Scrum and Cancelled to clear both the page
error and the book error when it is dismissed.
Closes#92
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
A first visit reported "Showing 2 of 3 items" with no hint that the
default filters hide cancelled work and all but today's finished work,
and with the preset controls disabled there was nothing to act on.
Add summarizeHiddenByStatus, which counts only the items a status
filter removed - items dropped by the text, tag or priority criteria
are not miscounted - and name the responsible statuses. The search bar
appends that sentence to the count, includes it in the live-region
announcement, and offers a Show hidden button that switches just those
statuses back to All.
Closes#89
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Every Books card showed the storage identifier as primary content -
a long opaque string that means nothing to the user and that leaks the
spreadsheet id for a Google Sheets book. Drop the line and keep the id
as a tooltip on the book name for support purposes.
Closes#90
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The board read New, Blocked, In Progress, Finished, so the normal
left-to-right progression skipped a column and then went backwards.
Blocked is an exception state entered from In Progress, so the lanes
now read New, In Progress, Blocked, Finished.
Cancelled is a task view like the boards but showed only the hamburger
button, making a return to a board two interactions. It now renders the
same Matrix/Scrum switcher.
Closes#91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
- Matrix cards no longer claim role="button" around their own status
buttons; the title is a real button, so the nested controls stay
reachable and the drag preview stops injecting a tab stop.
- The bump notice is suppressed for terminal items, and the calendar
store clears the remembered pre-bump date for any field the caller
reschedules, so a card can no longer claim Arrange moved dates the
user chose.
- Result and hidden counts share one clock, so they cannot straddle
midnight and disagree.
- The Cancelled bulk-delete failure path sets its message before the
reconciling refetch and preserves it, since a partial delete makes
the refetch the authoritative answer.
- Dropped the unused ErrorBanner action prop and the dead card focus
style; documented why the update overlay still closes on failure and
that the hidden count is scoped to loaded items.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
A multi-item calendar write can partly succeed and still reject, so the
snapshot restored after a failure is provisional until a refresh
confirms it. The refresh that follows such a failure was replacing the
write error with its own, leaving the user with restored rows and no
sign that the deletion or reorder had failed. Matrix, Scrum and
Cancelled now keep the original message and append the refresh failure
to it, so an unverified board says so.
Also surface the load window behind the item count as a tooltip, since
"Show hidden" cannot reveal work outside the fetched range.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
preserveError also covers the routine refresh that runs when a tab
regains focus, so composing on it meant every failed activation refresh
appended another sentence to the banner - unbounded, and claiming a
write could not be verified when none was attempted.
Add a distinct reconciling flag, set only by the paths that roll back a
failed write and carried through the deferred-fetch queue, and move the
composition into composeReconcileFailure, which collapses a repeated or
identical message instead of stacking it.
Also replace the hover-only load-window tooltip, which sat on an
aria-hidden span, with a scopeNote the page supplies: shown next to the
hidden-item notice and announced with it, and omitted on Cancelled,
which loads every item.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
A rollback after a failed write leaves the list provisional, because a
bulk write can partly succeed and still reject. The previous round
signalled that through an option threaded into the pending-fetch queue,
so only the very next refresh knew; a later failed refresh silently
dropped the warning. A ref that lives until a read succeeds is both
simpler and correct for every subsequent attempt.
Clearing it on success also clears the error, since an authoritative
read makes any message about an unverified write stale.
The scope note now renders and is announced with the count at all times,
not only beside a hidden-item notice, and says what the board actually
loads: 30 days either side of today.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The banner was being rebuilt from itself, which made it fragile in three
ways: a successful reconciliation erased the write failure the user had
not necessarily read yet, a manual refresh cleared it before reading and
so lost it if that read also failed, and a run of refreshes that failed
for different reasons appended a sentence each time.
The rollback now stores the write failure itself, and every banner is
derived from that fixed text: a failed read composes one refresh
sentence onto it, a successful read strips the sentence and keeps the
failure, and a manual refresh no longer clears it. Only a successful
read or the dismiss button retracts it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
The stored write failure had no book, so switching book while a rollback
was still unreconciled showed the other book's message and let a read
that verified nothing retract it.
The failure now carries its book id. A read only composes onto it, or
retracts it, when it is reading that same book; any other book drops it,
because the message belongs to work the user has navigated away from.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Dropping a foreign book's unverified failure left its message on screen
whenever the read that dropped it was preserving errors, and nothing
after that could retract it: the ref it was derived from was gone.
The fetch now clears the banner it is abandoning, but only when the
banner still starts with that failure - a message the user raised some
other way is left alone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Retracting an abandoned failure matched on its text, which misses the
shape the composer returns when the write and the refresh failed for the
same reason - it keeps none of the write text. Offline is exactly that
case, and the two were only kept apart by which fetch happens to run
first after a book switch.
The test now lives with the composer as bannerDerivesFrom, covering all
three shapes it can produce and leaving unrelated messages alone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Four findings, all real:
Dismissing the banner did not survive a read that was already in flight:
its failure reopened what the user had just closed. Reads started before
a dismissal no longer report.
Editing through the dialog left the board stale. Status changes made
there skipped the lifecycle timestamps the status buttons apply, and
setting a date did not drop the pre-bump original the store clears, so
the moved notice lingered. Both are now applied optimistically, in step
with what the store will do.
Errors said only what the backend said, so an offline save read as
"Failed to fetch" with no hint of which action failed. Every failure now
names its action first, with the technical detail after it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
Make date range and tooltip expectations locale-independent
src/arrange-v4/lib/bumpNotice.test.ts:35
These expectations are also tied to an English/US default locale even though the production formatters use the host locale. With de_DE, the text becomes 10. Sept. → 11. Sept. and the tooltip uses a different date/time order, so this suite fails outside an English locale. Derive locale-aware expectations or inject a fixed locale for the test.
Make date-time formatting test independent of host locale
src/arrange-v4/lib/dateUtils.test.ts:64
This assertion depends on the host's default locale, but formatAbsoluteDateTime intentionally calls toLocaleString(undefined, ...). For example, under de_DE the same value is 30.09.2026, 11:23, so npm test fails even though the formatter is behaving correctly. Make the test locale-independent (or make the formatter accept an explicit locale for deterministic tests).
The per-field reset lived only in the calendar store, so on Sheets a
rescheduled item lost its moved notice locally and got it back on the
next read.
The Sheets update path now clears the original for whichever date the
caller set, and marks it changed so the append records the clear rather
than leaving it to be read as untouched.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 112ac087-b7da-45a3-a00b-6ca9d06df52b
formatRelativeDate chooses relative versus absolute text independently for each endpoint, so ranges crossing its ±14-day cutoff still mix formats (for example, in 14d → Oct 15); the Actual range below has the same problem. This leaves the range inconsistency from #86 unresolved. Format each pair through a shared range formatter that chooses one mode for both endpoints.
Make the scope note backend-specific for Google Sheets
src/arrange-v4/app/matrix/page.tsx:1136
This scope note is inaccurate for Google Sheets books. GoogleSheetsStore.listItems returns every non-terminal item regardless of the requested window (lib/store/sheets/GoogleSheetsStore.ts:369-373), so the count can include active work older than 30 days while the UI says it cannot. Make the note backend-specific.
Make the scope note backend-specific for Google Sheets
src/arrange-v4/app/scrum/page.tsx:838
This scope note is inaccurate for Google Sheets books. GoogleSheetsStore.listItems returns every non-terminal item regardless of the requested window (lib/store/sheets/GoogleSheetsStore.ts:369-373), so the count can include active work older than 30 days while the UI says it cannot. Make the note backend-specific.
8 issues raised across all rounds - all fixed with code changes:
Round 1 (7 comments, 4 distinct issues): dismissing the banner did not survive a read already in flight (3 threads, one per page); dialog edits skipped the lifecycle timestamps the status buttons apply (2 threads); setting a date did not drop the pre-bump original, so the "moved" notice lingered until a refresh; failures showed only the backend's text, so an offline save read as "Failed to fetch" with no hint of which action failed. Fixed in 1c27a8a.
Round 2 (1 issue): the per-field original-date reset existed only in the calendar backend, so on Sheets the notice came back on the next read. Fixed in 1525723.
Round 3: clean - zero unresolved comments
Final status: all review threads resolved
Before the PR was opened, the branch also went through nine rounds with two independent reviewing models, ending in two consecutive clean rounds from both. 175 tests pass, tsc --noEmit and npm run build are clean, and lint is unchanged from main.
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
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.
Ten usability problems found by walking the app as an end user, each fixed in its own commit, plus the review fixes that followed.
Closes #82, closes #83, closes #84, closes #85, closes #86, closes #87, closes #89, closes #90, closes #91, closes #92.
(#88 was closed as not reproducible - the reported tab order was a Chrome sequential-focus artefact, not the app - and #92 was filed in its place.)
The ten fixes
useModalDialoggives them real dialog semantics, a two-way focus trap and focus restorearia-hidden; therole="status"string is the single announcementErrorBannercomponent, used by all three pagesWhat the review rounds changed
The rollback in #82 exposed a subtler problem: a rejected bulk write may still have partly landed, so the restored list is a guess until a read confirms it. That took several rounds to get right, and the result is:
Also from review: Matrix cards are no longer
role="button"containers wrapping their own status buttons, and the board's date-window note is exposed to assistive tech instead of living in a hover-onlytitleon anaria-hiddenspan.Verification
optimisticUpdate,useModalDialog,statusTimestamps,bumpNotice,hiddenSummary,reconcileMessage, plus additions todateUtils)tsc --noEmitclean,npm run buildclean, lint unchanged frommain