* docs(plugins): add microsoft 365 calendar provider plan
* docs(plugins): add microsoft 365 calendar provider plan
* docs(ios): plan internal testflight builds
* fix(sync): prevent file-based sync data loss
Preserve post-snapshot operations and stage remote baselines until durable apply.
Use conditional writes and a resumable marker for provider concurrency and legacy migration. Report trustworthy remote timestamps and document transport limits.
Refs #8960
* fix(sync): avoid biased conflict recommendations
Highlight a side only when its timestamp or known change count is strictly greater, leaving unknown and tied metadata neutral.
* fix(sync): preserve local edits during snapshot hydration
Extracts the one reachable-data-loss fix from the #9005 hardening branch
(feat/sync-recovery-hardening). On the default single-file path, a local
action dispatched while an async snapshot hydrate is in flight was
persisted against the old state and then clobbered by loadAllData (or
permanently dropped by markRejected) — a silent edit loss multi-device
users can hit on gap-detected re-sync.
Runs hydration inside writeFlushService.flushThenRunExclusive so capture
stays in deferred mode across the snapshot dispatch, commits the snapshot
baseline atomically (commitFileSnapshotBaseline), then replays the
buffered local intents and their archive side effects onto the new
baseline.
Deliberately excludes #9005's snapshot two-phase-commit, structural
validation, split-migration crash-resume, and gap persistence: reachability
analysis found those defend non-reachable (structural validation),
already-fail-safe (crash-mid-write), or default-off (split sync) failure
modes, at the cost of ~1k lines and two permanent on-disk formats.
* test(sync): add commitFileSnapshotBaseline to hydration spy
* docs(plugins): add microsoft 365 calendar provider plan
* docs(plugins): add microsoft 365 calendar provider plan
* docs(ios): plan internal testflight builds
* fix(locale): localize ISO calendar weekday/month names (#8987)
PR #8991 localized ISO 8601 weekday labels only in the custom
Schedule/Habits/Planner components. Material <mat-calendar> (schedule-task
dialog, deadline dialog, date-picker inputs, repeat-task heatmap) still
rendered month + weekday names via the global adapter locale 'sv' (the ISO
sentinel), so they showed Swedish and ignored the app language.
Override getDayOfWeekNames, getMonthNames and the spelled-out branch of
format() in CustomDateAdapter to run under a temporary swap to
isoTextLocale() (the UI language) when the ISO option is active. Numeric
dateInput and time-only formats keep the configured locale, so ISO stays
YYYY-MM-DD and the 24h clock is preserved. The swap assigns this.locale
directly (not setLocale) so it fires no spurious localeChanges.
Add unit coverage for the adapter and a real <mat-calendar> integration
spec asserting UI-language headers, a live language switch, and the
non-ISO fallback.
* docs(plugins): add microsoft 365 calendar provider plan
* fix(jira): gate Electron requests behind one-shot capability
Claim privileged Jira IPC before plugin startup and return responses through invoke instead of a broadcast event. Keep arbitrary HTTP(S) Jira hosts supported while rejecting redirects and bounding request resources.
* fix(jira): enforce Electron request capability
Bind privileged Jira IPC to a main-issued renderer-document token and strip raw Electron events from renderer callbacks. Scope image authentication by origin, base path, and resource type while preserving safe redirects and legacy configurations.
* fix(electron): handle payload-only IPC lifecycle
Clear Jira image authentication before replacement and when a new renderer document claims the capability. Parse before-close IDs from payload-only events so pending sync and finish-day hooks can complete.
* fix(electron): address Jira IPC capability review findings
- electron.effects: read ANY_FILE_DOWNLOADED payload at [0] after the
payload-only IPC refactor (was [1], now undefined -> TypeError on every
download); guard against a malformed payload
- jira-capability: rotate the token on re-register so a renderer reload
that reuses the WebFrameMain object is not permanently locked out of
Jira; invalidates any stale token
- document that the one-shot consumption order, not the bypassable
main-frame check, is the real capability boundary
- jira-electron-bridge: skip the no-op clearImgHeaders IPC round-trip
when image auth was never set up (non-Jira detail-panel open/close)
- jira-api: route a synchronous _toElectronRequestInit throw through
_handleResponse instead of leaking a dangling request-log entry
* test(electron): cover ANY_FILE_DOWNLOADED payload parsing
Extract parseDownloadedFilePayload from ElectronEffects and add a
regression spec pinning the payload-only shape ([file], not [event,
file]) that caused a TypeError on every download. Hardened against
non-array input surfaced by the new test.
* fix(electron): restore Node ambient globals for frontend build
* docs(plugins): add microsoft 365 calendar provider plan
* docs(plugins): add microsoft 365 calendar provider plan
* docs(ios): plan internal testflight builds
* fix(sync): serialize archive replacements
* test(sync): scope archive-race spec to op-log replacement (#8941)
The integration spec's first case drove SyncHydrationService.hydrateFromRemoteSync
and expected it to serialize against archive compression via the TASK_ARCHIVE
lock. But the hydration-side lock is not part of this PR — it lands with the
snapshot-hydration ordering change in PR #9010 (fix/8960-hydration-race), which
also ships its own sync-hydration.service.spec coverage.
Because hydrateFromRemoteSync never requested the lock here, that test hung
forever on `await hydrationLockRequested` while compressArchive still held the
real navigator.locks `sp_task_archive` lock. The leaked global lock then
cascaded into every downstream spec that acquires it (26 failing tests,
LockAcquisitionTimeoutError).
Drop the hydration-path case (it belongs with #9010) and keep the
runRemoteStateReplacement case, which is exactly the writer this PR serializes.
Trim the now-unused hydration providers/mocks from the setup.
deleteProject cascade-deletes a project's tasks, notes, sections, repeat
config, and archive data in one reducer pass. When that op lost an LWW
conflict to a concurrent project edit, only the PROJECT entity was
reversed: every client resurrected an empty project and the winning
client's status-blind hydration replay cascaded its tasks away after a
restart (live state != post-restart replay).
Rather than recreate every cascaded entity (payload scales with project
size and cannot restore every side effect safely), give schema-v4
deleteProject operations explicit delete-wins precedence:
- new deleteProject actions carry a shared PROJECT_DELETE_WINS_MARKER; the
shared LWW planner accepts a host-supplied delete-wins classifier. A
marked remote delete is applied regardless of timestamps; a marked local
delete is replaced with one op whose vector clock dominates both sides.
- historical unmarked (schema-v3) deletions keep timestamp-based LWW; the
absence of the marker (never added by the no-op v3->v4 migration) is the
real discriminator, and a schema v3->v4 barrier (mirroring v2->v3) makes
older clients block on the newer-schema gate instead of mis-resolving.
Delete-wins plans reuse the archive-win resolution pipeline, so they
inherit its atomic persistence and losing-op rejection, and disjoint
merge leaves them untouched (the delete must win the whole entity).
Hardening from multi-agent review:
- union allTaskIds/noteIds across multiple concurrent marked deletes for
the same project, so a single replacement cannot leave orphan tasks on
clients that only receive it (the task reducer removes by allTaskIds).
- gate the classifier on the AUTHENTICATED payload projectId matching the
plaintext entityId, so a tampered/replayed delete retargeted onto a live
entity cannot silently drop a concurrent edit.
- guard a null/undefined delete payload in the classifier instead of
throwing and wedging the conflict pass.
- pin the server's legacy-misc conflict alias to the fixed v1->v2 split
boundary, not CURRENT_SCHEMA_VERSION, so this bump does not fabricate
false GLOBAL_CONFIG:misc/tasks conflicts during rollout.
- bind the marker with a shared const (compiler-checked on producer and
consumer) and rename _isArchivePlan -> _isWholeEntityWinPlan.
Documents the policy as ARCHITECTURE-DECISIONS.md #7.
Addresses #8997.
* docs(plugins): add microsoft 365 calendar provider plan
* docs(plugins): add microsoft 365 calendar provider plan
* docs(ios): plan internal testflight builds
* fix(sync): prevent file-based sync data loss
Preserve post-snapshot operations and stage remote baselines until durable apply.
Use conditional writes and a resumable marker for provider concurrency and legacy migration. Report trustworthy remote timestamps and document transport limits.
Refs #8960
* fix(sync): avoid biased conflict recommendations
Highlight a side only when its timestamp or known change count is strictly greater, leaving unknown and tied metadata neutral.
* test(sync): assert options arg on split-file processRemoteOps
The split-file snapshot path now routes post-snapshot ops through
_processRemoteOpsWithStartupCleanup, which calls processRemoteOps with an
(empty) options object. Update the assertion to match the 2-arg call so the
unit suite passes.
* refactor(sync): dedupe strong-ETag regex, document snapshot-op boundary
- Extract the duplicated RFC 7232 strong-entity-tag pattern into a single
STRONG_ETAG_RE constant with a comment noting why the char class is safe to
interpolate into an If-Match header (no CR/LF header splitting).
- Explain why sv === undefined ops are classified as snapshot-included: they are
legacy migration ops fully contained in the snapshot, and _validateSnapshotRef
enforces a clock-EQUAL boundary. No behavior change.
* fix(tasks): make REST project moves atomic
Capture affected subtasks in the persisted update action so project moves replay consistently, and repair stale project and section references during archive and restore.
Fixes#8983
* fix(tasks): harden project move replay
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* @
feat: suppress idle dialog during focus mode sessions
Add a new setting (default: ON) to suppress the idle time dialog when a
focus mode session is actively running. The idle popup no longer
interrupts Pomodoro, Flowtime, or Countdown sessions.
Changes:
- Add isSuppressIdleDuringFocusMode field to IdleConfig
- Add checkbox to Settings → Time Tracking → Idle Handling
- Skip idle trigger in idle.effects.ts when focus session is active
- Add translations for all 28 languages
Closes: #1834
References: #1676
@
* feat: add opt-in setting to suppress idle dialog during work sessions
Address review feedback: narrow scope, default OFF, fix existing-user config merge.
Changes:
- default: isSuppressIdleDuringFocusMode changed from true to false (opt-in)
- reducer: deep-merge idle section on loadAllData so the new field defaults
properly for users with persisted idle configs
- i18n: revert all non-English translation files, keep only en.json
- effects: fix Prettier/ESLint indentation
- tests: add reducer test for idle section merging + idle-effects spec
- docs: add setting to Settings-and-Preferences.md
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: repair idle-effects test harness for CI
- Use ReplaySubject<void>(1) so onReady$ replay survives late subscription
- Provide LOCAL_ACTIONS token + provideMockActions
- Fix TS2740 type mismatch on chromeInterfaceMock.onReady$
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(tasks): avoid double-offsetting add-task bar on ios
Use the overlay-only keyboard offset on iOS because Capacitor's native resize mode already shrinks the WebView. Preserve the measured keyboard-height path for Android and other touch builds, with regression coverage for both selectors.
* fix(tasks): scope ios keyboard offset to touch devices
Keep hybrid iOS devices on the existing top-positioned layout while applying the overlay-only keyboard offset to touch-only iOS builds. Replace the stylesheet inspection with rendered layout coverage for resized, overlay, non-iOS, and hybrid cases.
* test(planner): reset mocked selectors after each test
Prevent the mocked task selector from leaking into later Jasmine specs and causing nondeterministic sync convergence failures.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* chore: add project-scoped Angular MCP
* chore: update npm for release-age policy
* fix(sync): preserve LWW outcomes across clients
Distinguish replacement snapshots from partial merge operations, protect device-local sync configuration, recreate winning deletes, and resolve every conflicted entity in bulk operations.
Fixes#8956
* fix(sync): harden mixed LWW conflict replay
Preserve unaffected remote and local bulk intents, keep device-local sync settings during replacement replay, and gate replacement semantics behind schema v3.
* fix(sync): recover subtask subtree and harden LWW replay follow-ups
Follow-up hardening for #8956 after multi-agent review:
- Recreate a locally-winning parent's subtasks when a remote bulk delete
is a mixed winner. The full remote delete is applied (cascade-deleting
the parent's subtasks via handleDeleteTasks) but only the parent had a
compensation op, so the subtree was silently lost across devices.
- extractUpdateChanges: scan array-valued payload props instead of guessing
`${payloadKey}s`, so irregular bulk keys (e.g. taskUpdates) no longer
return {} and drop a remote winner's changes.
- Degrade gracefully instead of throwing when a remote update wins over a
local delete with no reconstructable base entity, matching the
single-entity path (a permanent sync wedge is worse than the bounded
divergence it already accepts).
- Remove dead code (unused `deleting` set + zero-caller wrapper), use the
Set-based scoped-bulk-delete filter, type meta via LwwUpdateMode, and
restore the withLocalOnlySyncSettings rationale comment.
Adds regression tests for the subtree-recovery and irregular-bulk-key paths.
* fix(sync): preserve conflict outcomes during replay
Persist replacement LWW operations and bulk-delete snapshots so reconstructed state matches the result applied live. Bump the op-log DB version to prevent older clients from opening the incompatible schema.
* fix(sync): persist conflict outcomes atomically
Write remote losers, local compensations, and final remote winners in one IndexedDB transaction so crashes cannot expose a partial replay order. Add real-store coverage for live/replay equivalence and transaction rollback.
* fix(sync): preserve multi-entity conflict recovery
* fix(sync): close review gaps in multi-entity conflict recovery
- recreate a winning parent's subtasks when a remote DELETE loses
outright (single-entity or all-local-win bulk), so clients that
applied the delete and status-blind hydration replay converge
- apply the combined resolution batch in durable seq order so a
pending row reused from a prior failed attempt replays identically
live and after a crash
- restamp converted remote updates carrying the v3 replacement
envelope to the current schema version
- strip the virtual TODAY tag from LWW task payloads and shallow-merge
patch-mode singleton payloads instead of replacing feature state
- pin the server snapshot fast-path spec to CURRENT_SCHEMA_VERSION
(fixes the CI failure from the v2-to-v3 bump)
* test(sync): pin outright-losing delete convergence across clients
Three-way real-reducer/real-store convergence for the pure-loser path
(live == restart replay == originating client), covering both the
same-batch recreate exemption and the cross-batch recreate path.
Verified to fail against the pre-fix service.
* fix(ui): open time picker for Electron touch input
Open the native time picker for touch activation in Electron while preserving mouse, keyboard, pen, and non-Electron behavior.
Closes#8986
* test(planner): reset mocked selectors after scheduling specs
Counter-scale fixed-size font icons by the Android WebView text zoom while preserving accessible text and inline icon scaling. Covers Angular icons, raw Material Symbols, and shared pseudo-element icons.
Fixes#5694
* fix(sync): keep the conflict summary banner counts live during review
The banner captured its counts when it opened; the sync-icon badge
updates live but an OPEN banner went stale while the user reviewed
entries on the sync-conflicts page (and lingered with a nonzero count
after everything was reviewed).
Refresh (or dismiss at zero) the banner on every unreviewed-count
change — but only while it is actually still shown, so a banner the
user dismissed is never resurrected by reviewing activity. Adds
BannerService.isShown(id) for that check.
SPAP-35 (deferred LOW from PR #8874 review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: retrigger CI (supersync crash-resume e2e flake)
* fix(sync): guard live-refresh banner against dismiss/reorder/phantom-zero races
Follow-up hardening to the SPAP-35 live-refresh banner, from an
independent review of the change. The refresh does an async journal
read between the "is the banner shown?" check and the open/dismiss that
follows, and that gap could misbehave three ways:
- Resurrection: the banner's own DISMISS button bypasses this service,
so a dismiss landing mid-read left the post-read open() to resurrect
the banner the user just closed. Re-check isShown() AFTER the read.
- Stale overwrite: bursts of count changes (e.g. "Keep All" marking
every entry) start concurrent refreshes with no ordering guarantee,
so a slow older read could reopen after the zero-count dismiss or
show a stale count. Add a shared monotonic sequence guard across the
open and refresh paths — last write wins.
- Phantom zero: ConflictJournalService.list() degrades to [] on a
transient DB error, so a zero read while the count stream still
reports >0 must not dismiss a valid banner.
Each guard has a regression test that fails if the guard is removed.
No change to the open-on-sync behavior.
SPAP-35
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sync): make the live conflict banner mutation-aware and coalesced
Addresses maintainer review on #8946:
- Trigger the open-banner refresh on a journal REVISION (a new monotonic signal
bumped on every mutation), not distinctUntilChanged on the unreviewed count, so
an equal-total composition change (one remote-win reviewed while one local-win
is recorded, total unchanged) still refreshes the 'X remote, Y local' breakdown.
- Coalesce mutation bursts (e.g. Keep All over the whole journal) into a single
trailing refresh via auditTime, so a bulk review no longer fires one full
journal scan per entry.
- Phantom-zero guard now reads the authoritative unreviewedCount signal.
Also fixes the rebase onto current master: #8945 migrated unreviewedCount$ to a
signal, so the banner's old Observable subscription no longer compiled.
Adds regression tests: an equal-total (1 remote -> 1 local) composition change
refreshes the breakdown (fails on the old count-keyed trigger), and a burst of
reviews coalesces to a single journal scan.
SPAP-35
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(sync): enforce conflict-journal retention mid-session, not only at start
pruneOnStart (14 days / newest 200) ran only at startup, so a long-lived
session could grow SUP_CONFLICT_JOURNAL unboundedly. record() now checks
the store count (cheap) and, when it exceeds JOURNAL_MAX_ENTRIES plus a
slack of JOURNAL_PRUNE_SLACK, runs the same age+count prune — amortized
to once every JOURNAL_PRUNE_SLACK records. The prune core is extracted
and shared with pruneOnStart; the observe-only never-throw contract of
record() is unchanged.
SPAP-36 (deferred LOW from PR #8874 review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sync): await tx.done with the deletes in the conflict-journal prune
An aborted prune delete transaction rejects both the delete requests and
tx.done. Awaiting them separately (Promise.all(deletes), then await tx.done)
leaves tx.done's rejection unhandled once the delete aggregate rejects first, so
it escapes as a global unhandled rejection during active conflict resolution.
Await both in one Promise.all so tx.done always has a handler. Adds an
aborted-transaction regression test (fails on the old sequencing) and updates
the retention model comment + doc to reflect mid-session pruning.
Addresses maintainer review on #8948.
SPAP-36
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(sync): prevent destructive full-state sync races
Use causal ordering and server-sequence preconditions for full-state operations, surface snapshot rejection, deduplicate migrations, and retain queued WebSocket downloads.\n\nFixes #8959
* fix(sync): block uploads after rejected imports
Keep incremental operations behind a durable barrier when a local import or restore is rejected. Release the barrier only after a newer full-state snapshot is accepted, and surface the blocked state instead of reporting sync success.
* fix(sync): harden causal repair recovery
Persist repair base cursors across the client, provider, and server so stale snapshots are rejected before quota or history mutation and legacy repairs cannot poison restore state.
Atomically rebase stale repairs after downloading the missing suffix, preserve rejected-import barriers and WebSocket watermarks, and cover PostgreSQL serialization.
* fix(sync): harden repair recovery edge cases
Block dependent uploads after any rejected full-state operation, reset negotiated capabilities across provider config changes, and bound WebSocket download retries.
Update sync test doubles for causal repairs and the expanded duplicate-operation identity.
* fix(sync): reject forged encrypted full-state operations
Validate decrypted import and repair payloads against the full-state schema so authenticated ordinary operations cannot be promoted into destructive full-state operations.
Closes#8905
* fix(sync): preserve compatible encrypted full-state payloads
Normalize only known wire and legacy omissions on the validation copy so stripped device-local settings and pre-section backups are not mistaken for metadata tampering.
* test(sync): cover encrypted full-state round trips
* fix(sync): honor sync import conflict outcomes
Propagate nested conflict cancellation through rejected-op handling so it cannot trigger automatic merges or consume the retry budget.
Report force-local resolution failures when the clean-slate upload is blocked, rejected, or accepts no operations.
* fix(sync): harden force overwrite recovery
Verify the exact force-upload operation, preserve clean-slate retries, and roll back rejected replacements. Keep unresolved work retryable without reporting a successful sync.
* test(sync): cover clean-slate rollback in postgres
* fix(sync): make task replay values deterministic
Capture logical dates and timestamps in task actions and backfill legacy operations. Carry per-day task totals so own-operation replay is idempotent while foreign time remains additive.
Fixes#8957
* fix(sync): make time snapshots replay-safe
Exclude pending task-time batches from op-log and file-sync snapshots so their later additive operations cannot overlap snapshot state. Preserve concurrent direct credits and normalize legacy replay dates deterministically.
Fixes#8957
* fix(sync): align snapshots with queued task time
Exclude accumulator and in-flight task-time deltas from operation-log snapshots so later delta operations cannot double-count them. Capture file and direct-upload snapshots at the same operation-log boundary, and flush or clear queued time around destructive state replacement.
* fix(tasks): flush queued time at task boundaries
Persist queued timer deltas before absolute short-syntax edits, and clear them whenever project, schedule, or repeat cleanup deletes the owning task. This prevents stale batches from recreating or overwriting removed task time.
* fix(sync): preserve concurrent task time deltas
Treat concurrent task-time batches as commuting updates on both client and server, while retaining causal stale-operation checks. Reject malformed identities, dates, durations, and unsafe timestamps before replay or persistence.
* test(sync): cover task time snapshot replay
Exercise seeded snapshot and restart invariants plus a real three-client SuperSync convergence path with the initial time inside the snapshot.
* fix(sync): select action type for legacy conflicts
* fix(sync): reject disjoint merges for multi-entity ops
Scope conflict field extraction and journal titles to the actual entity. Fall back to whole-op LWW whenever either side is multi-entity so sibling updates are never falsely reported as preserved.\n\nFixes #8944.
* fix(sync): harden multi-entity conflict resolution
* fix(sync): guard full-state apply against late local ops
Recheck pending work inside the upload and operation-log locks before destructive imports. Defer same-tab actions through the cutoff and keep cursors and acknowledgements unchanged when dialog resolution is required.
Closes#8310
* test(sync): cover late multi-tab full-state race
Use the real Web Lock boundary and shared IndexedDB to prove a sibling-tab operation blocks destructive full-state apply.
* fix(sync): preserve offline operations and harden auth
Accept operations from long-offline clients without rewriting their timestamps, and retain a replayable full-state base during cleanup. Neutralize registration account discovery and suppress repeated login and recovery token emails with atomic claims.
Fixes#8961
* fix(sync): harden auth and retention edge cases
Make unauthenticated auth responses neutral across delivery and resend failures, isolate WebAuthn ceremonies, and consume login and recovery tokens atomically. Add monitored handling for histories without a replay base and strengthen regression coverage.
Refs #8961
* perf(sync): reduce cleanup replay-base queries
Reuse the maintained full-state sequence marker during retention cleanup while preserving a query fallback for legacy and stale-marker rows. Clarify no-base warnings and strengthen exact auth-response and recovery transaction tests.
Refs #8961
* fix(sync): reject non-integer op timestamps before BigInt persistence
A non-integer or non-finite client timestamp passed the schema
(timestamp is z.number(), not .int()) and threw at BigInt() during
upload, aborting the whole batch as an unstructured 500. Reject it in
validateOp as a per-op INVALID_TIMESTAMP instead. Op age stays
unbounded so long-offline backlogs are still accepted.
Refs #8961
PROJECT flips dispatch updateProject, whose snackUpdateBaseSettings$
effect popped an unconditional "Project updated" snack on top of the
flip flow's own outcome reporting. Add an optional isSkipSnack flag to
updateProject (mirroring updateTask's isIgnoreShortSyntax precedent)
and set it from the flip path.
Also adds a MockStore.resetSelectors() afterEach to the conflict-ui
spec: overrideSelector mutates the shared selector references, and the
new selectProjectById override otherwise leaks into project.service
specs in the same karma bundle.
SPAP-40 item 1 (from the PR #8874 independent review). Item 2 (clearAll
outside the op-log lock) is already fixed on master; item 3 (local-won
post-resolution stale baseline) is a documented follow-up and stays in
the ticket.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
matBadgeDescription is a string input; binding null relied on
AriaDescriber.describe() silently no-op'ing on falsy values. Verified
hidden-state semantics: matBadgeHidden hides the badge via CSS only and
MatBadge keeps an aria description active regardless, so at count 0 the
description must be empty — '' is the type-correct value MatBadge
explicitly removes the aria-describedby for.
SPAP-38 (deferred LOW from PR #8874 review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The repo prefers Signals; unreviewedCount$ was a BehaviorSubject-backed
Observable whose only consumer immediately converted it with toSignal().
Expose a readonly signal directly instead.
SPAP-37
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The archive-mutex comment claimed every archive mutation is locked;
compressArchive, the remote loadAllData import and the time-tracking
cleanups still write outside it (tracked in #8941). Name them.
- The merge-journal comment described an impossible skipped-duplicate
path (merged ids are fresh per run); describe the real accepted
window instead: durable merge, crash before journaling, entry stays
absent (observe-only log).
- Delete the unreachable stage-2 intra-batch duplicate pass: stage 1
(validateAndClampBatch) reserves every op id including invalid first
siblings, so validated candidates are unique by construction.
- Delete clearRawRebuildIncomplete: superseded by completeRawRebuild,
which retires the marker atomically with the recovery token; only
specs still called it.
- Clear the conflict journal when a USE_REMOTE rebuild completes — the
documented "cleared whenever the full dataset is replaced" contract
previously had a single caller (backup import), leaving stale badge
counts and review entries describing replaced history.
- _notifyResolutionOutcome: drop the win-count parameters left over
from the removed count snack and gate on resolutions.length.
- Snapshot handler: keep the clean-slate opId invariant local with a
defense-in-depth 400 instead of relying on the contract superRefine
in another package.
- Document that the legacy misc->tasks conflict alias only covers the
per-entity path (GLOBAL_CONFIG writes are single-entity today).
isIgnoreDBLock historically meant "sync already holds the op-log DB
lock", but _runTaskArchiveMutation also treated it as permission to skip
the new TASK_ARCHIVE mutex — so every remote archive side effect except
moveToArchive ran unserialized against locked local mutations, and a
concurrent read-modify-write could silently drop one side's archive
write (the exact race the mutex was added to close).
TASK_ARCHIVE is deliberately separate from OPERATION_LOG, so acquiring
it while sync holds the op-log lock is safe — the remote moveToArchive
path has always done exactly that. The mutex is now unconditional, and
the remote flushYoungToOld handler wraps its two-archive read-modify-
write in the same lock instead of writing through the adapter bare.
The isIgnoreDBLock option is retained as inert API surface; removing
the threading is tracked as a follow-up.
Review of the #8874 x #8900 merge seam found two defects in STEP 3b:
- The synthesized merged op rode in the apply batch, so the reducer
checkpoint's pending-only assertion threw on it: every real disjoint
auto-merge aborted the checkpoint transaction and wedged sync behind
IncompleteRemoteOperationsError until app restart. Checkpoint now
covers only pending-appended remote rows; synthetic local ops are
exempt (their durability contract is the append + upload path).
- appendWithVectorClockUpdate replaced the durable vector clock with a
clock computed only from the conflict's own ops, regressing the
client counter and enabling silent cross-device op drops. Merge
writes now go through appendMixedSourceBatchSkipDuplicates, which
rebases the merged op on the durable clock in the same transaction —
also closing the crash window between the remote originals and their
superseding merged op, and turning duplicate re-appends into skips
instead of ConstraintErrors.
Two regression tests enforce the coordinator's whole-batch reducer-
commit contract and the pending-only checkpoint against the resolution
flow; the journal keeps recording merges only after a durable append.
Same #8874-vintage store mock as the disjoint-merge spec: without the
atomic mixed-source batch method every local-wins flow through
autoResolveConflictsLWW threw before journaling.
The #8874 spec predates the atomic mixed-source batch and reducer
checkpoint on the store port; its two createSpyObj sites lacked the
methods, so every flow through the local-wins write path threw.
The lock-serialization refactor routes internal archive calls through
_updateTasks/_deleteTasks so a held sp_task_archive lock is never
re-acquired; five assertions still spied the public wrappers and never
fired.
Update the archive-operation lifecycle docs to the atomic
reducer-checkpoint + clock-merge design, record the IndexedDB v8
downgrade barrier, drop the removed PENDING_OPERATION_EXPIRY_MS
constant, and describe the durable Use-Server-Data Undo across reloads.
The misc-to-tasks migration spread the transformed legacy misc values
over an already-populated tasks section, so a stale v1 misc copy could
overwrite settings a v2 client had since changed. Flip the merge order
(existing tasks section wins) and drop the already-migrated early
return, which made the outcome depend on which duplicate arrived first.
- A request repeating one operation id no longer double-charges quota or
wedges the batch: the first occurrence reserves the id and later
siblings are terminally rejected (DUPLICATE_OPERATION for an exact
retry, INVALID_OP_ID otherwise), in both batch and serial paths.
- Clean-slate snapshot uploads become durably idempotent: the request
cache is process-local and expiring, so the client-supplied opId is
now required (contract superRefine) and checked against the stored
operation inside the per-user lock before any data is deleted. An
exact retry returns the original serverSeq; a colliding opId is
rejected without touching existing data.
- Audit logging validates id/entityType/opType/clientId against the
known-safe charsets before embedding them in log lines, and the six
duplicated reject-audit blocks collapse into one rejectedUploadResult
helper; the ops handler logs rejected error codes instead of whole
operation objects.