Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/IMPLEMENTATION_MASTERPLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Last Updated: 2026-09-10

Finish delivery of #2895 and its bounded #2896 reminder-save ownership follow-up. The latter replaces a component-wide boolean with a request identity so an older account's completion cannot discard a newer account's unsaved hours. Its reproduced interleavings, full frontend suite, typecheck/build and lint pass. Keep the final #2808 completion record tied to the merged delivery receipts.

The #2893/#2894 recovery follow-up is implemented in [PR #2895](https://github.com/Chris0Jeky/Taskdeck/pull/2895): preserve reminder drafts, report concurrent question writes accurately and align model payloads with the evidence preview. Keep the existing privacy/revision/usage boundaries, prove the reproduced cases and publish one reviewed follow-up. Local full suites, focused final payload checks, browser recovery and bounded independent review pass.

Merged #2892 as 93eab3443 after required run 34453924959 passed at reviewed head 42965edde. The reminder-hours slice under #2808 delivers: optional named-zone weekly windows, explicit save/recovery, existing shared budgets and server-side eligibility. Local full backend/frontend, combined privacy/recovery and cross-browser proof pass. The final active-account write condition prevents erased private hours from being restored by an in-flight request. Old opted-in users retain unrestricted behavior; reminders remain default off.
Expand Down
2 changes: 2 additions & 0 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Last Updated: 2026-09-10

Reminder save ownership (#2896): a delayed completion from a previous account can no longer release the guard for a newer account's enable save. Both success and failure interleavings were reproduced before repair; 22 targeted component cases and the full 6,438-test frontend suite pass, with three existing skips. Typecheck, production build and scoped lint pass. This is a focused follow-up to #2895; the delivery tracker records its hosted gate and merge receipt.

Final overhaul recovery follow-up (#2893/#2894, [PR #2895](https://github.com/Chris0Jeky/Taskdeck/pull/2895)): retain an unsaved hours draft across enable-only saves and confirmed validation rejections, distinguish competing question writes from source changes, and send the model exactly the previewed excerpt. Final frontend passes 6,436 tests with three existing skips; full backend passes 9,315 with 34 existing skips, followed by 17 API and 19 application tests for the final payload correction. Build/typecheck, scoped lint, browser recovery and bounded independent review pass. Required hosted runs 34458797270 and 34462673996 passed at reviewed heads f39cc6cf3 and d25edf2a5, including both operating-system suites and browser smoke. The PR records subsequent base qualification and the final merge receipt. The source and draft regressions were reproduced before repair; the validation ledger records their scope.

Optional reminder hours (#2808, merged #2892): explicit weekday and time windows in an IANA zone, including overnight and daylight-saving behavior. Server checks precede question lookup and budget admission; preference edits preserve the shared UTC allowance, old-client toggles retain the window, and exports include it without a new migration. The slice passed 9,270 backend and 6,421 frontend tests, with 34/three existing skips respectively. After parent integration, 56 API tests, 56 component tests, typecheck/build and five Chromium journeys pass, including a reproduced and fixed account-erasure race. Firefox and mobile Grove/Grove Night evidence are recorded in the validation ledger. Required CI run 34453924959 passed at reviewed head 42965edde; #2892 merged as 93eab3443. [Policy](product/WORKSPACE_ATTENTION.md).
Expand Down
13 changes: 13 additions & 0 deletions docs/product/WORKSPACE_OVERHAUL_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Workspace overhaul validation and follow-through

## Reminder save ownership follow-up (2026-09-10)

Issue #2896 covers a late-review account-switch interleaving: account A's save is pending, account B
loads settings and starts another enable save, then A settles before B. Both resolved and rejected A
responses reproduced replacement of B's unsaved zone with its saved zone. A unique request identity
now lets only the owning completion release the component guard. Both regressions pass within 22
attention component tests and assert an explicit later hours save using B's returned revision.

Full frontend qualification passes 6,438 tests with three existing skips across 416 files; typecheck,
production build and scoped ESLint pass. The backend is unchanged. The deterministic component tests
exercise the actual shared store and account-generation checks; no new physical-device or live-provider
claim is made. The follow-up PR records its independent review and hosted delivery receipt.

## Final recovery follow-up (2026-09-10)

The hours draft regression failed in both enable-toggle directions before the repair. A confirmed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const restricted = ref(false)
const zone = ref('UTC')
const days = ref<number[]>([1, 2, 3, 4, 5])
const start = ref('09:00'); const end = ref('17:00')
let savingEnablement = false
let savingEnablement: symbol | null = null
const dayOptions = [{ value: 1, name: 'Monday' }, { value: 2, name: 'Tuesday' }, { value: 3, name: 'Wednesday' },
{ value: 4, name: 'Thursday' }, { value: 5, name: 'Friday' }, { value: 6, name: 'Saturday' }, { value: 0, name: 'Sunday' }]
const formatTime = (minutes: number) => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`
const minutes = (value: string) => { const [hour, minute] = value.split(':').map(Number); return hour! * 60 + minute! }
watch(() => attention.settings, settings => {
// An enable-only receipt must not replace an independent, unsaved hours draft.
if (settings && savingEnablement) return
savingEnablement = false
savingEnablement = null
const window = settings?.window
restricted.value = !!window
zone.value = window?.timeZoneId ?? 'UTC'
Expand All @@ -29,9 +29,10 @@ const valid = computed(() => !restricted.value || (zone.value.trim().length > 0
function localZone() { zone.value = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' }
async function saveEnabled(enabled: boolean) {
if (attention.busy || !attention.settings) return
savingEnablement = true
const request = Symbol()
savingEnablement = request
try { await attention.save(enabled) }
finally { savingEnablement = false }
finally { if (savingEnablement === request) savingEnablement = null }
}
function saveHours() {
if (!attention.settings || !valid.value) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ describe('optional quiet reminders', () => {
expect(useWorkspaceAttentionStore().settings?.enabled).toBe(true)
})

it.each(['resolve', 'reject'])('keeps the newer account save guarded when the old save settles by %s', async outcome => {
wrapper = mount(WorkspaceAttentionSettings); await flushPromises()
let completeOld!: (value: typeof settings) => void
let rejectOld!: (reason: Error) => void
vi.mocked(workspaceAttentionApi.save).mockReturnValueOnce(new Promise((resolve, reject) => {
completeOld = resolve; rejectOld = reject
}))
await wrapper.get('input').setValue(false); await flushPromises()
session.userId = 'different-owner'; await flushPromises()
const savedWindow = { timeZoneId: 'Europe/London', daysMask: 62, startMinute: 540, endMinute: 1020 }
vi.mocked(workspaceAttentionApi.get).mockResolvedValueOnce({ ...settings, revision: 10, window: savedWindow })
await wrapper.get('button').trigger('click'); await flushPromises()
await wrapper.get('input[type=text]').setValue('America/New_York')
await wrapper.findAll('input[type=time]')[0]!.setValue('22:00')
await wrapper.findAll('input[type=time]')[1]!.setValue('02:00')
let completeNew!: (value: typeof settings & { window: typeof savedWindow }) => void
vi.mocked(workspaceAttentionApi.save).mockReturnValueOnce(new Promise(resolve => { completeNew = resolve }))
await wrapper.get('input').setValue(false); await flushPromises()
if (outcome === 'resolve') completeOld({ ...settings, enabled: false, revision: 2 })
else rejectOld(new Error('Old account save response was lost'))
await flushPromises()
expect(useWorkspaceAttentionStore().busy).toBe(true)
expect(useWorkspaceAttentionStore().settings?.revision).toBe(10)
completeNew({ ...settings, enabled: false, revision: 11, window: savedWindow }); await flushPromises()
expect((wrapper.get('input[type=text]').element as HTMLInputElement).value).toBe('America/New_York')
expect((wrapper.findAll('input[type=time]')[0]!.element as HTMLInputElement).value).toBe('22:00')
expect((wrapper.findAll('input[type=time]')[1]!.element as HTMLInputElement).value).toBe('02:00')
const window = { timeZoneId: 'America/New_York', daysMask: 62, startMinute: 1320, endMinute: 120 }
vi.mocked(workspaceAttentionApi.save).mockResolvedValueOnce({ ...settings, enabled: false, revision: 12, window })
await wrapper.get('form').trigger('submit'); await flushPromises()
expect(workspaceAttentionApi.save).toHaveBeenCalledTimes(3)
expect(workspaceAttentionApi.save).toHaveBeenLastCalledWith(11, false, window)
})

it('saves an explicit weekly window and disables controls until its receipt arrives', async () => {
wrapper = mount(WorkspaceAttentionSettings); await flushPromises()
const restrict = wrapper.findAll('input[type=checkbox]')[1]!
Expand Down
Loading