mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
462 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8e5ed2c15 | fix(plugins): harden node execution grants | ||
|
|
3d2c811e78 | chore(task-repeat): revert RRULE Phase 1 from master (#7948) Develop the full RFC-5545 RRULE epic on the long-running feat/rrule-epic branch and merge once complete and testable in final form, rather than landing 13 phases into master one half-state at a time. Work preserved on feat/rrule-epic. Not yet shipped (post-v18.9.1), so no user impact. | ||
|
|
870d4a4882
|
docs(backup): document Android automatic restore (#8193)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
140a0367ea
|
docs(caldav): clarify bidirectional VTODO sync (#8196)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
1718b0a8b8
|
feat(task-repeat): RFC 5545 RRULE recurring schedules — EPIC · Phase 1/13 (Closes #4020) (#7948)
* feat(task-repeat): add cron / natural-language recurring schedules
Add a CRON repeat cycle so a single recurring task can express schedules
the day/week/month/year cycles cannot (e.g. every Saturday, March through
November). The cron expression drives occurrence generation; all other
schedule fields are ignored for CRON cfgs.
- Occurrence engine: getNewestPossibleCronDueDate / getNextCronOccurrence in
cron-occurrence.util.ts (cron-parser), wired into the existing
getNewestPossibleDueDate / getNextRepeatOccurrence dispatchers. Recurrence
stays day-granular: sub-daily crons create at most one task per day.
- English to cron via the crono-eng WASM module (src/assets/crono-eng.wasm,
loaded lazily), with a builtin regex parser as fallback until/if the module
is unavailable. naturalLanguageToCron() also passes raw cron through.
- Cron to english live preview under the input via cronstrue; the field shows
the interpreted cron + humanized reading as the user types and warns when an
expression is sub-daily.
- Invalid / unrecognized input shows an error snack and blocks save.
- Add-task bar: @+<cron-or-phrase> short syntax attaches a CRON repeat cfg.
- tools/build-crono-wasm.js regenerates the WASM asset from the sibling
crono-eng Zig project (committed asset is the source of truth for builds).
* fix(task-repeat): cron edge cases, clearer warnings, and corpus tests
Hardening + UX pass on the cron repeat cycle, driven by verifying the full
crono-eng corpus (415 phrases) end to end.
Fixes
- Silent never-fires: crono-eng can translate phrases (a specific year, "nearest
weekday"/W, "n-to-last day"/L-n, biweekly #-lists) into Quartz forms the
recurrence engine (cron-parser) cannot run. These passed UI validation yet a
task would never be created. naturalLanguageToCron now gates its output on
cron-parser, so such input is rejected at the field instead.
- Off-by-one for midnight crons: cron-parser's next() is exclusive of an
exact-boundary currentDate, so getNextCronOccurrence skipped the first
eligible day for 00:00 schedules (e.g. first occurrence landed a day late).
Seed the search one ms before the lower-bound day so a midnight fire stays
eligible; now matches the non-cron daily convention.
UX
- Live preview shows the interpreted cron + plain-English reading below the
field, and warns when the time of day is ignored (day-granular engine) or the
schedule is sub-daily ("runs at most once per day").
- Friendlier validation/snack message with examples instead of terse jargon.
Tests
- tools/test-crono-wasm.js (npm run test:crono): corpus-driven harness asserting
the committed WASM matches all 415 corpus crons, and that cron-parser +
cronstrue accept them — classifying the 27 known engine-unsupported forms so
it stays a regression guard.
- cron-occurrence.util.spec.ts: occurrence engine across daily/weekly/monthly/
month-range/sub-daily, varied times, start-date and last-creation gating.
- parse-natural-cron.util.spec.ts: raw passthrough, phrase recognition, null
cases, loader resilience.
- task-repeat-cfg-form.cron.spec.ts: field validator + live-preview/time-warning.
* fix(task-repeat): live cron preview + @+ title strip; add E2E
Fixes found by adding end-to-end coverage of the cron feature.
- Live preview did not render: Formly's mat-hint does not reflect a dynamic
expressionProperties.description, so the interpreted-cron/English preview never
updated as the user typed. Move it into the dialog template, driven by a
computed signal off the form's valueChanges (getCronPreview in cron-preview.util),
so it updates live and persists after applying. The time-of-day-ignored and
sub-daily warnings now render as proper translated lines.
- "@+<phrase>" short syntax left the clause in the title: shortSyntax set
taskChanges.title for the cron strip, but the later parseTimeSpentChanges
reassignment wiped it, so a bare "Foo @+every day" submitted the raw title.
Strip into the working title and emit the cleaned title at the end.
- shortSyntax returned cronExpression unconditionally (undefined when absent),
breaking 38 exact-match assertions; only include the key when set.
Tests
- e2e/tests/recurring/cron-repeat.spec.ts: @+ short-syntax attaches a CRON cfg
(title stripped); full dialog flow (phrase → live preview + time warning →
save → CRON cfg persisted).
- short-syntax.spec: @+ extraction/stripping, with and without other syntax.
- getCronPreview unit tests (interpreted cron, English, timed/sub-daily flags).
* test(task-repeat): property/fuzz/metamorphic/edge cron tests; fix first-occurrence
Large second testing pass (≈+58 unit tests, +2 E2E, new property harness),
which surfaced one gap and one upstream quirk.
Fix
- getFirstRepeatOccurrence returned null for CRON cfgs (no case → default), so a
timed CRON task's first instance and the startDate-moved-earlier path fell
back to today instead of the cron's first fire. Add getFirstCronOccurrence
(first fire on/after startDate) and route CRON through it.
Tests
- cron-occurrence.invariants.spec: property/invariant battery over many crons
(no-throw, result strictly-after / on-or-before bounds, determinism,
monotonicity), calendar edges (leap-day, year rollover, DST spring/fall),
pathological "Feb 30" (null, no hang), and getFirstCronOccurrence /
getFirstRepeatOccurrence routing.
- parse-natural-cron.metamorphic.spec: relational tests (case/whitespace/order/
irrelevant-text invariance, idempotence, determinism, raw passthrough).
- short-syntax.spec: more @+ cases (raw cron after @+, bare @ is not cron,
unrecognized phrase ignored, clause stops at the next delimiter).
- tools/test-crono-properties.js (npm run test:crono:props): WASM determinism,
marshalling fuzz (empty/oversized/unicode/boundary), English→cron→English
round-trip, and occurrence simulation across every runnable corpus cron.
- e2e: invalid phrase blocks save + shows inline error (Save disabled);
raw cron expression preview + save. Shared dialog-open helper.
Note: documented a cron-parser DST quirk — prev() skips the spring-forward
midnight, so "newest due" can land a day earlier on that date (day-granular,
once a year; lastTaskCreationDay prevents duplicates).
* test(task-repeat): selector integration, serialization, and WASM buffer-reuse
- selectors.spec: CRON cases for the selectors that drive task creation —
selectAllUnprocessedTaskRepeatCfgs (due today, overdue, already-created,
paused, future start, invalid expr, deleted instance) and
selectTaskRepeatCfgsForExactDay (exact-day match). Proves a CRON cfg is
picked up by addAllDueToday's selector path.
- cron-occurrence.invariants.spec: a CRON cfg survives a JSON round-trip with
identical occurrence behavior (sync / backup integrity).
- test-crono-properties.js: buffer-reuse checks — a short translation after a
long one is uncontaminated, and results stay deterministic across a 600-call
interleaved loop (the WASM module reuses static input/output buffers).
* test(task-repeat): cross-timezone day-resolution harness
The Karma suite only runs in the host timezone (Chrome on Windows ignores the
TZ env var — the reason the pre-existing *.tz.spec fails), so cron day-resolution
was never verified across zones. Add a Node harness (npm run test:crono:tz) that
spawns a child process per timezone — Node honors TZ — and asserts day-class
invariants in each: weekly-Monday resolves to a Monday, monthly day-15 to the
15th, daily to the next calendar day, time-of-day never shifts the day, and a
full-year daily iteration is monotonic with 366 distinct days (no DST skip in
next()). Covers fractional offsets (India +05:30, Chatham +12:45/+13:45) and
Southern-hemisphere DST (Sydney). Mirrors getNextCronOccurrence's core math.
* fix(task-repeat): DST-safe newest-due; verify real engine across timezones
Closes the time-based reliability gaps from the previous testing pass.
Fix
- getNewestPossibleCronDueDate no longer uses cron-parser's prev(), which skips
the spring-forward midnight in DST zones (a daily cron then looked like it
didn't fire that day). It now walks days backward and asks "does the cron fire
during this local day?" via a next()-based probe (next() is monotonic across
DST). Spring-forward and fall-back now resolve the exact day.
Tests
- main.ts exposes the real occurrence utils on __e2eTestHelpers.cron (dev/stage
only, stripped from production).
- e2e/cron-timezone.spec: runs the REAL engine under five forced browser
timezones (Playwright timezoneId — reliable regardless of host OS, unlike the
Karma TZ env). Each asserts the timezone was actually applied (live UTC
offset) AND that day-class results are correct, incl. fractional offsets
(Chatham +12:45) and the spring-forward day.
- test-crono-tz.js: also asserts a daily cron fires on every calendar day of the
year in all 8 zones (regression guard for the prev() skip).
- invariants.spec: spring-forward / fall-back now assert the exact day for both
next() and newest (no longer hedged).
- effects.spec: a CRON cfg's first occurrence is computed through the real
updateTaskAfterMakingItRepeatable$ effect path (live new Date()).
* test(task-repeat): dev jumpDay clock helper + live day-change e2e
Adds __e2eTestHelpers.jumpDay(n) / resetClock() (dev/stage only, stripped from
production) so recurring/day-change behavior can be fast-forwarded from the
DevTools console without touching the OS clock: jumpDay shifts Date.now() by a
cumulative day offset and forces the day-change re-sample so addAllDueToday()
runs. New e2e asserts jumpDay(1) creates the next daily instance — covering the
live day-change -> creation path end to end.
* feat(task-repeat): add "create a task for each missed occurrence" option
By default, opening the app after several scheduled occurrences were
missed creates only a single instance for the most recent occurrence.
When enabled (advanced section), a separate overdue task is created for
every missed occurrence instead, oldest first, capped at the 30 most
recent to avoid flooding the list / emitting a large sync batch.
- new getAllMissedDueDates() util reuses getNextRepeatOccurrence so the
per-cycle and CRON occurrence logic stays single-sourced and DST-safe
- service multi-spawn path is gated to the catch-up flow (target day is
today or earlier) and is mutually exclusive with skipOverdue /
waitForCompletion; the single newest-occurrence path is unchanged
- form checkbox hides when "skip overdue" is set, and vice versa
Part of #4020
* fix(task-repeat): re-anchor lastTaskCreationDay when cron expression changes
cronExpression was missing from SCHEDULE_AFFECTING_FIELDS, so editing a
CRON schedule did not re-run rescheduleTaskOnRepeatCfgUpdate$. A
previously computed (often future) lastTaskCreationDay then stuck around
and silently suppressed every occurrence until real time caught up to it
— e.g. a "Jan-Aug" cron set while the clock was past August anchored to
Jan 1 next year and produced no tasks in between.
Adding 'cronExpression' makes a cron edit re-anchor from the current
date, consistent with repeatCycle / repeatEvery / weekday edits.
Part of #4020
* feat(tasks): surface @+ recurring short syntax in autocomplete and help
The `@+<schedule>` add-task short syntax existed but was undiscoverable.
- add recurring examples to the `@` autocomplete suggestions: typing `@+`
now autocompletes phrases like `@+every monday` or
`@+every saturday from march through november`
- document `@+` in the Settings -> Short Syntax help text
Part of #4020
* fix(tasks): remove @+ recurring autocomplete entries
The `@+` examples added under the `@` mention trigger kept the suggestion
dropdown open while typing `@+<phrase>`, so pressing Enter selected a
suggestion instead of submitting the task — breaking the headline
type-and-go flow. The `@+` syntax stays documented in Settings -> Short
Syntax; only the dropdown entries are removed.
Part of #4020
* refactor(task-repeat): show cron only inside Custom recurring config
Cron appeared twice in the recurring-config dropdown: as a top-level
"Cron / natural language" quick-setting AND as a cycle inside "Custom
recurring config". Drop the duplicate top-level option; cron is now
reached via Custom -> repeatCycle = Cron.
- remove the CRON quick-setting option from the dropdown builder
- keep the repeatCycle select visible when CRON is chosen (only hide
repeatEvery) so the user can switch back
- map existing cron configs (incl. legacy quickSetting 'CRON') to CUSTOM
on load so the dropdown matches the stored cycle
- update the cron E2E to drive the dialog via Custom -> Cron
Part of #4020
* test(task-repeat): make @+ short-syntax e2e date-independent
The test asserted the `@+every monday` task appears in the Today view, but
a weekly schedule's first instance is the next Monday — so on any non-Monday
the task is correctly scheduled for a future day and isn't in Today, making
the test fail depending on the day it runs. Verify the attached CRON config
and the stripped title via the store instead, which is view- and
date-independent.
Part of #4020
* feat(task-repeat): RFC 5545 RRULE recurring schedules [WIP]
Overhaul recurring tasks from the bespoke repeatCycle format to RFC 5545
RRULE as the canonical recurrence representation (legacy fields kept
populated for old-client forward-compat). Adds a structured RRULE builder,
RRULE-backed quick presets, per-instance due-date derivation, multiple end
conditions (COUNT / UNTIL / after-N-completions), an occurrence heatmap with
completion simulation, natural-language @+ short syntax, and a REST API for
recurring tasks. Migration is lazy (on dialog open) so existing configs keep
firing untouched until edited.
WIP: some required features and tests are still outstanding, and it needs
much more real-world (user-based) testing before it is merge-ready.
* feat(task-repeat): per-occurrence overrides, due-date control move, @+ preview, NL fix [WIP]
- RECURRENCE-ID: per-instance overrides (move / re-time / re-title) surfaced to
the engine as RDATE + EXDATE so every projection stays consistent.
- Move the due-date derivation control out of the rrule builder into the dialog
so it applies to all recurring configs (presets + Custom).
- Live @+ recurrence preview (humanized rule + next occurrence date) in the
add-task-bar, so a far-off rule is obvious before submit.
- Fix @+ "every other <weekday> in <month>" mis-parsing to YEARLY;INTERVAL=2
("every 2 years"); weekly-style interval + weekdays now stays WEEKLY.
* refactor(task-repeat): trim PR to engine+builder+migration+preview; fix curator review
Trim the WIP recurring-schedules PR to its reviewable core — RFC 5545 occurrence
engine, structured RRULE builder, legacy<->RRULE migration (both directions),
live text preview, quick-setting presets, and tests — and defer the rest to
follow-up sub-MRs: natural-language @+, due-date derivation, ends-after-N-
completions, missed-occurrence backfill, heatmap preview + simulation, REST
recurring creation, and RECURRENCE-ID per-instance overrides. The full
implementation is preserved on branch feat/recurring-full.
Curator review fixes:
- quickSetting forward-compat: never persist a value outside the released
(master) union. The newer preset literals AND 'RRULE' map to 'CUSTOM' at every
persist boundary (dialog save, add-task-bar, data-repair downgrade); the rich
value drives the dialog UI in-memory only and the builder reconstructs from the
opaque rrule on open. Fixes old/mobile typia sync-validation treating such
tasks as corrupt.
- Malformed rrule now falls back to the legacy schedule fields (guarded by
isRRuleValid) instead of silently stopping the task.
- Reverse converter maps a BYDAY-less FREQ=WEEKLY onto the start weekday, so the
legacy WEEKLY engine still fires on old clients.
- Stop logging the raw rule body (the raw-override field makes it free-text user
input; log history is exportable).
- DRY: share normalizeWeekdays / toNumArray between the two converters.
* refactor(task-repeat): clamp quickSetting at the action creators
The op-log replays the action payload, not reduced state
(operation-capture.service.ts), so a reducer-level clamp would not keep an
out-of-union quickSetting off the wire. Clamp in the addTaskRepeatCfgToTask /
updateTaskRepeatCfg / updateTaskRepeatCfgs creators instead -- the single
boundary every dispatcher passes through, so old/mobile clients always replay a
value their typia union accepts. The newer preset literals and 'RRULE' stay
in-memory in the dialog form only.
Removes the now-redundant per-call-site clamps in the dialog save path and the
add-task-bar; the @+ and REST paths inherit the clamp for free when their phases
return. Adds an action-creator spec covering the clamp at each entry point.
* feat(task-repeat): nth-weekday rows support multiple weekdays per ordinal
Each ordinal row (1st/2nd/3rd/4th/last) now multi-selects weekdays via toggle
buttons, so one line can mean "the 1st Monday and the 1st Tuesday" -> BYDAY=1MO,1TU.
The per-row ordinal dropdown excludes positions already used by other rows (each
ordinal anchors at most one line), and the add button hides once all are used.
Parsing groups weekdays that share an ordinal back into one row. Applies to both
the monthly and yearly nth-weekday modes; updates the helper copy accordingly.
* test(task-repeat): complex RRULE coverage + day-by-day create-loop sim
Adds high-complexity coverage for the occurrence engine and builder:
- engine variants x settings: per-day ordinals, BYSETPOS last-weekday,
BYMONTHDAY=-1, seasonal BYMONTH, BYWEEKNO/BYYEARDAY, leap years, mixed with
COUNT / UNTIL / EXDATE / lastTaskCreationDay
- builder forward + mode-detection + lossless round-trips (incl. multi-weekday
ordinals and sub-daily raw-override)
- cfg->engine routing for complex rules with deletedInstanceDates and the
malformed-rrule legacy fallback
- a "day-march" simulator that drives getNewestPossibleDueDate one day at a time
with lastTaskCreationDay fed back like the service, asserting the created
stream has no dupes/skips, honors EXDATE/COUNT, is idempotent within a day,
and documents Phase-1 app-closed catch-up (newest missed only)
Also trims the deferred @+ short-syntax e2e (returns with its phase) and fixes
the dialog-flow e2e to target the current .rrule-result preview selector.
* feat(task-repeat): custom ordinal input for nth-weekday rows and BYSETPOS
The nth-weekday ordinal dropdown and the weekday-set 'which occurrence'
dropdown each gain a 'custom…' option that reveals a free-form input,
allowing any non-zero ordinal (e.g. -2 = 2nd-to-last, 5FR) and
comma-separated BYSETPOS lists (e.g. 2,-1). Parsed rules with such values
now render structurally instead of falling back to the raw override.
* feat(task-repeat): make weekday-set occurrence (BYSETPOS) a multi-select
Replace the single-value 'which occurrence' dropdown with toggle buttons
matching the weekday toggles, so several occurrences can combine (e.g.
first + last weekday = BYSETPOS=1,-1). 'Every' clears the narrowing; the
custom input stays for values without a toggle.
* fix(task-repeat): address review findings on rrule migration fidelity
- Yearly builder rules seed BYMONTH from the start month: a bare
FREQ=YEARLY;BYMONTHDAY=n expands across every month (RFC 5545) and
fired monthly for fresh yearly configs.
- quickSetting change handler passes the selected start date, so
date-writing presets no longer overwrite a user-picked future anchor
with today.
- Remove the createForEachMissed checkbox + copy: the engine has no
backfill support yet, so the setting silently did nothing (and wrote
an undeclared field into synced state).
- rruleToLegacyTaskRepeatCfg always resets the monthly anchor fields so
stale nth-weekday/last-day anchors can't survive the merge, sets
monthlyLastDay only for a pure BYMONTHDAY=-1 rule, and aligns
startDate to the rule's first occurrence for date-anchored
monthly/yearly rules (legacy clients read day/month from startDate);
save() re-derives the fallback so later startDate edits stay aligned.
- _normalizeMonthlyAnchor keeps monthlyLastDay on RRULE saves — it is
the derived old-client fallback for BYMONTHDAY=-1, not a stale preset
flag.
- Legacy CUSTOM migration preserves clamp semantics: day > 28 anchors
emit BYMONTHDAY=<d>,-1;BYSETPOS=1 (the day, or the last day of
shorter months) instead of a plain BYMONTHDAY that skips such months;
the builder serializer emits BYSETPOS in day-of-month modes so these
rules round-trip structurally.
- Regenerate package-lock.json to drop stale cron-parser/cronstrue
entries left over from the earlier cron approach.
* fix(task-repeat): harden rrule builder + legacy fallback after deep review
Correctness:
- Clear bySetPos (and its custom-input state) on monthly/yearly mode and
frequency switches: a BYSETPOS left over from the weekday-set mode
silently narrowed day-of-month rules (BYMONTHDAY=15;BYSETPOS=2 never
fires) with no UI to see or clear it.
- Move startDate alignment out of rruleToLegacyTaskRepeatCfg into a new
arithmetic getAlignedStartDate, applied once at save and only when the
schedule actually changed: the occurrence-search version corrupted the
clamp idiom (aligned a day-31 rule onto Feb 28/29 so old clients fired
on the wrong day forever), collapsed multi-day BYMONTHDAY lists to
their earliest day, rewrote the visible start-date field live on every
builder interaction, cost ~60ms/keystroke for sparse rules, and put
startDate into the change diff on title-only edits — retriggering the
issue-#7373 reschedule. Weekday-anchored rules are no longer aligned.
- Emit the monthly-anchor resets as null/false instead of undefined (in
the converter AND the presets' MONTHLY_ANCHOR_RESET): JSON.stringify
drops undefined keys from op-log payloads, so the reset never reached
remote clients and stale nth-weekday/last-day anchors survived there.
The anchor model fields now allow null; the nth-weekday engine guard
strips it.
- Enforce the YEARLY BYMONTH guard in the serializer: yearly date mode
without months now emits a plain FREQ=YEARLY (anniversary) instead of
a bare BYMONTHDAY that fires monthly; parsed bare yearly rules fall
back to the raw override, preserving their semantics verbatim.
- Drop the auto-seeded BYMONTH when switching away from YEARLY (it
silently constrained the new rule to one month) and seed from the
CURRENT start date rather than the ngOnInit-captured month.
- Clamp custom nth ordinals per frequency (±5 monthly, ±53 yearly) —
BYDAY=10MO is valid RFC but matches nothing, ever — and reject an
ordinal another row already anchors (duplicate rows collapse on
reload). Re-clamp poses when switching into MONTHLY.
- Drop BYSETPOS=0 on parse (parseString accepts it; re-emitting creates
a rule the occurrence engine silently treats as dead).
- Predefined set-position toggles close the explicitly-opened custom
input (both rendered active with contradictory state).
Cleanup:
- Extract parseIntList (4 cloned int-list sanitizers), pushBySetPos /
pushByDaySet (4 copy-pasted emit blocks), _clampedMonthDayPart
(monthly/yearly clamp duplication); share the monthly/yearly
weekday-set template block via ngTemplateOutlet; parse BYSETPOS via a
computed signal instead of per-CD string parsing.
- Correct the BYMONTHDAY hint: it claimed short months clamp to their
last day, but plain BYMONTHDAY skips them.
* test: make timezone demo specs host-independent
Six .tz.spec demonstration tests branched on the host's CURRENT
getTimezoneOffset() (or a literal 'America/Los_Angeles' name check)
while asserting against January test instants. In DST-observing zones
the summer offset differs from the January one (e.g. New York: EDT 240
vs EST 300), so the wrong branch was taken and the suite failed every
summer on US-east machines, blocking pre-push hooks.
Branch on the offset AT the test instant instead, with the correct
longitude threshold for each instant (07:00 UTC flips the local day at
UTC-7, 06:00 UTC at UTC-6, midnight UTC at UTC±0) — the tests now pass
in any host timezone year-round.
* test: pin browser timezone to Europe/Berlin in karma config
Re-enable the previously commented-out TZ pin so timezone-sensitive
specs run deterministically where Chrome honors the env var
(Linux/macOS, incl. CI). Verified empirically that headless Chrome on
Windows IGNORES TZ and keeps the host zone — the *.tz.spec.ts files
keep their offset-at-test-instant branching as the Windows backstop.
* test: make supersync dump/restore helpers cross-platform
createFullDump/restoreFullDump used POSIX-only shell constructs (output
redirection to /tmp, 'cat | psql', 'rm -f'). On Windows the restore
pipeline failed AFTER the 'DROP SCHEMA public CASCADE' had already
succeeded, leaving the test database without any tables — every
subsequent spec in the run then failed in createTestUser with 'table
public.users does not exist' (5 cascading failures + dozens of
never-ran tests).
Capture pg_dump via execSync stdout + fs.writeFileSync, feed the
restore through stdin (execSync input), use os.tmpdir() and
fs.unlinkSync. Verified: full dump-restore spec plus the three spec
files it previously poisoned all pass on Windows (14/14).
* test(e2e): round-trip coverage for new rrule builder widgets
Covers the four gaps hand-testing would otherwise need to catch, using
the dialog's live result band as the oracle (.rrule-result__expr =
exact assembled rule, .rrule-result__next = engine-computed upcoming
dates — no waiting for real time):
- custom nth ordinal (-2 = 2nd-to-last weekday) emits the right BYDAY
and persists verbatim
- BYSETPOS multi-select (first + last) emits BYSETPOS=1,-1 and persists
- mode switch drops a weekday-set BYSETPOS instead of leaking it into a
day-of-month rule (dead-rule regression)
- switching to YEARLY seeds BYMONTH and the upcoming dates are strictly
one per year
Persistence asserted via the NgRx store: saving a recurring cfg
re-plans the task onto its first occurrence (usually not today), so a
UI reopen from the work view is date-dependent; the parse-to-widget
rendering side is covered by the dialog/builder unit specs.
* fix(add-task-bar): open the repeat dialog for the custom recurring option
The repeat quick-setting menu emits the 'RRULE' value for 'Custom
recurring config', but the add-task-bar still branched on the legacy
'CUSTOM' value — picking the option fell through to the preset path and
silently created a weekly-fallback cfg instead of opening the dialog.
Route both 'RRULE' and 'CUSTOM' through the dialog path, preselect the
RRULE builder in the dialog via a new initialQuickSetting dialog input
(the user explicitly asked for a custom rule), and show the tune icon
for the menu entry again. Covered by a new e2e: menu pick → task
created → builder dialog opens → saved rule persists verbatim.
* fix(task-repeat): address review on rrule alignment + anchor sync safety
- getAlignedStartDate re-anchors onto the rule's actual first occurrence
(occurrence-set-neutral for INTERVAL/COUNT/UNTIL) instead of pure date
math that shifted INTERVAL cadence and skipped valid clamped occurrences
- save() always re-derives the legacy fallback fields against the final
startDate, not only when alignment moved it
- monthly anchors never persist null (released clients' typia schema only
allows absent-or-numeric): undefined resets everywhere, null stripped at
the action-creator boundary, model reverted to master signature
- round-trip guard ignores BYSETPOS=0 so the cleaned rule is emitted
instead of being preserved verbatim via raw override
* docs(wiki): fix MD024 duplicate headings on repeating-tasks page
* fix(task-repeat): harden rrule save guards + restore preset labels on reopen
Correctness (from deep review of the branch diff):
- reject COUNT combined with repeatFromCompletionDate at save: completion
re-anchors startDate/lastTaskCreationDay, restarting the COUNT window, so
the series would never terminate
- reject parseable rules that yield zero occurrences (e.g.
FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30): they persisted as silently dead
recurrences with the legacy fallback bypassed
- map the builder's single-weekday BYSETPOS form (BYDAY=FR;BYSETPOS=-1,
'last Friday') onto the legacy nth-weekday anchor; old clients previously
fell back to startDate's day-of-month — a wrong recurrence
Polish:
- infer the preset back from the stored rrule when quickSetting was
wire-clamped to CUSTOM, so Weekends & co. reopen under their label
instead of the raw builder; new QUICK_SETTING_PRESETS single source
replaces the hand-maintained KNOWN_PRESETS set
- extract shared safeParseRRuleOptions + FREQ_TO_CYCLE (six drifted
hand-rolled parse wrappers); memoize isRRuleValid (per-cfg routing guard
on every overdue scan); share noonUtc/toLocalNoon between engine and
preview; fold toMonthArray onto toNumArray; merge the duplicate
_toPersisted action-creator helpers
- trim the wiki section documenting the create-for-each-missed feature
that was deliberately cut from this PR
* fix(task-repeat): address curator review on anchor validity + fallback phase
- never emit/persist an out-of-union monthlyWeekOfMonth: range-guard the
BYDAY ordinal in rruleToLegacyTaskRepeatCfg (the builder's custom input
allows ±5, the synced model only 1..4|-1), sanitize null AND out-of-range
anchors at the action-creator boundary, and mirror the repair in
data-repair — an out-of-union value trips released clients' typia
validation/repair flow
- reject sub-daily frequencies (raw override FREQ=HOURLY/...) at save: the
day-granular engine would accept but silently collapse them to ~daily,
and they have no legacy repeatCycle for old clients
- log hygiene: the update-all-instances flow now logs changed keys + task
ids instead of raw changes/task objects (title, notes and rrule body are
user content; the log history is exportable); engine warns log the error
name only, since rrule.js messages can embed the free-text rule body
- align single-BYDAY weekly rules onto the engine's first occurrence: the
engine groups weeks by WKST while the legacy fallback counts rolling
7-day blocks from startDate, so biweekly cfgs whose start is off the
pattern day fired on OPPOSITE alternating weeks on old vs new clients
(and WKST shifted the engine's phase, which legacy cannot express);
after re-anchoring the cadence is exactly interval*7 days in both
- diff dialog saves against the PROCESSED cfg: the lazy legacy->rrule
migration (and preset inference) no longer leak into the change set of a
title-only edit — rrule is schedule-affecting, so that leak relocated
today's live instance on unrelated edits; empty change sets skip the
update dispatch entirely instead of creating a no-op sync op
* fix(task-repeat): keep presets rrule-backed + builder mode for completion cfgs
- stop stripping the preset-generated rrule at save: getQuickSettingUpdates
OVERWRITES the rule with the preset's canonical one, so the old 'clear on
switch away from builder' branch broke the every-cfg-carries-its-rrule
contract — and an rrule:undefined clear would not even propagate (dropped
by the op-log JSON wire, leaving remote clients on the old rule); the spec
asserting the stale behavior is inverted
- completion-relative cfgs always open in builder mode: the schedule-type
toggle only exists there, so preset inference (or a stored faithful preset
label) would hide the one control that explains how the cfg fires
- monthly anchor clears: document the wire gap properly instead of flipping
values again — null trips released clients' blocking data-repair confirm
dialog on every sync (typia has no null on these fields), undefined clears
only locally; durable clears require shipping '| null' on both fields in a
release FIRST, then switching the reset value (sequenced migration note in
the model + an op-log JSON round-trip spec pinning current behavior)
- replace 6 hardcoded rrule-builder placeholders with translation keys
- revert the settings help text advertising the deferred @+ short syntax
* test(e2e): drop CUSTOM weekday-checkbox spec superseded by rrule builder
The #8025 e2e (merged in from master) drives the legacy CUSTOM recurring
form's cycle select + weekday checkboxes, which this branch removes — the
RRULE builder replaces that UI and its weekday toggles are covered by
rrule-builder-roundtrip.spec.ts. The #8025 failure mode (a saved weekly
cfg that never fires) is blocked generally at save by the first-occurrence
probe.
* test(e2e): de-flake worklog history day-row wait
The worklog is rebuilt from the archive on the history navigation, so the
day row only appears once that async load + month-expand animation settles.
The bare `waitFor` fell back to the 15s action timeout, which CI load
(prod build, 3 workers) could exceed. Use a web-first `expect().toBeVisible()`
with 30s headroom instead.
* fix(task-repeat-cfg): clear repeat-from-completion when switching to a preset
The "from completion" schedule-type toggle exists only inside the RRULE
builder. Selecting a preset hides it, but the flag stuck on the cfg, so a
preset could keep firing relative to completion with no visible control.
Clear it at the save() edit boundary, but only when actually set, so an
untouched preset save stays an empty-diff no-op (#7373 class) rather than
dispatching a spurious undefined->false op. The ADD path already starts from
a false default, so the dialog is the only path that needs it.
Also reword the monthly-anchor clear comments: the cross-client divergence on
an nth-weekday -> day-of-month switch is an inherent, unfixable limitation for
pre-rrule clients (no in-schema "no anchor" value; a `| null` migration would
help an empty client band), not a deferred TODO. Make the round-trip test pin
this explicitly and start the monthlyLastDay case from `true` so it proves the
`false` clear survives the wire instead of passing vacuously.
|
||
|
|
5314126b93
|
feat(azure-devops): configure auto import limit (#8092)
* feat(azure-devops): configure auto import limit #7017 * fix(azure-devops): translate auto import limit form #7017 --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
5be516a02c
|
fix(calendar): disable iCal polling on Android (#8173)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
a0374e2769
|
fix(markdown): toggle fullscreen checklist labels #5950 (#8109)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
e1499af73a
|
docs(calendar): note per-device credentials (#8185)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
ba31037d0f
|
fix(worklog): avoid misleading export start/end times (#8090)
* fix(worklog): avoid misleading export times #5654 * chore: rerun worklog export checks * fix(worklog): show session times once per day #5654 --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
3ef8413b57
|
fix(config): clear shortcut with delete keys #5604 (#8142)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
f0a9e38c20
|
fix(backup): add Android automatic backup restore (#8066) (#8158)
* build: drop unused elevate.exe from Windows build #8135 elevate.exe is bundled by electron-builder's NSIS target solely so electron-updater can apply privileged per-machine updates. We ship no in-app auto-updater (electron-updater is not a dependency; the autoUpdater block in electron/start-app.ts is commented out), so the binary is unused dead weight and a frequent AV false-positive. Set packElevateHelper: false to stop bundling it; safe because perMachine is not true. * docs: flag task component (perf) and sync system (correctness) as high-risk areas * fix(backup): add Android automatic backup restore Adds a settings action for restoring the latest Android automatic backup without dirtying config state. Reuses the existing mobile backup import flow and documents the restore path. * fix(backup): harden manual mobile backup restore Require a usable mobile backup before restoring from Settings and use confirmation copy that warns current local data will be replaced. * refactor(backup): address review on mobile restore action Restore the config-section items-vs-customSection render invariant (the action row is a sibling block, so loosening the guard was unnecessary) and document why the isUsableBackupStr gate is not redundant with the ring loader (selectBestBackupStr can return a corrupt fallback blob). * feat(backup): confirm restore success and warn about cross-device replace - Show a success snack after a Settings restore completes (was silent). - Make the confirm copy honest that restore propagates to other synced devices on next sync; document the same in the wiki. - Add config-section.component.spec covering the onAction pending/error paths. - Note that the iOS branch is intentionally unwired (Android-only per #8066). * refactor(backup): dedupe mobile backup-load ternary Extract _loadBestMobileBackupStr() — the identical isAndroidWebView ? loadBackupAndroid() : loadBackupIOS() ternary was duplicated across the startup and settings restore paths. * test(config): destroy fixture in config-section spec teardown Defensive teardown so the spec can't leak DOM/fixture state into later specs in the shared Karma run. * test(layout): restore document.activeElement clobbered in focus specs The Focus restoration specs override document.activeElement via Object.defineProperty but never restored it, so the static value leaked into the shared Karma session and made TaskService.focusTaskById specs fail intermittently (random spec order). Delete the override in afterEach, mirroring task-shortcut.service.spec. |
||
|
|
0e04e8feaa
|
feat(tasks): checklist progress, bulk actions & notes editor UX (#8156)
* build: drop unused elevate.exe from Windows build #8135 elevate.exe is bundled by electron-builder's NSIS target solely so electron-updater can apply privileged per-machine updates. We ship no in-app auto-updater (electron-updater is not a dependency; the autoUpdater block in electron/start-app.ts is commented out), so the binary is unused dead weight and a frequent AV false-positive. Set packElevateHelper: false to stop bundling it; safe because perMachine is not true. * docs: flag task component (perf) and sync system (correctness) as high-risk areas * feat(tasks): show checklist progress indicator on task cards Derive checklist progress from a task's markdown checklist notes and surface a compact "done/total" badge in the task controls. The badge switches to a completed state when every item is checked and opens the notes panel on click, making checklist progress visible at a glance without opening the task. https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * feat(tasks): add checklist reorder and bulk actions in notes Add drag-to-reorder for checklist items in the notes editor and a checklist actions menu (Check all / Uncheck all / Clear completed), shown only when the notes are recognized as a markdown checklist. All operations are backed by pure, unit-tested helpers that manipulate the existing markdown notes string and emit through the normal change flow, preserving interleaved prose lines. https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * refactor(tasks): harden checklist reorder and add drop indicator Review follow-ups for the checklist actions: - guard moveChecklistItem against NaN/non-integer indices (a NaN drop index previously slipped past the range checks and corrupted ordering) - show a drop-target highlight while dragging a checklist item - drop redundant modelCopy.set (the model setter already syncs it) - add component tests for bulk actions and the drag-and-drop flow https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * refactor(tasks): remove checklist drag-to-reorder, refine card icon Remove native drag-to-reorder for checklist items in the notes editor (drag handlers, drop indicator, the moveChecklistItem helper, and the related styles and tests). The checklist bulk-actions menu (check all / uncheck all / clear completed) and the task-card progress badge are kept. On the task card, the checklist progress badge now stands in for the plain 'chat' notes indicator: when a task has a checklist the redundant notes icon is suppressed, and the badge itself yields to the close (X) button when the detail panel is open, mirroring how the notes icon behaves. * feat(tasks): replace unmodified default notes template on checklist toggle When the notes field shows only the app's stock default template (and it has not been edited), the checklist button now replaces that placeholder with a fresh checklist instead of appending a checkbox below it. Edited content and user notes are preserved and appended to as before. Adds a `defaultText` input on inline-markdown so the task detail panel can pass the (replaceable) stock template, guarded by isStockNotesTemplate so a user-customized template is treated as real content. * fix(tasks): indent list line on Tab regardless of cursor column handleTabKey previously only indented when the cursor sat at line start or at the end of an empty prefix, so pressing Tab mid-item moved focus out of the editor instead of indenting. It now indents whenever the collapsed cursor is on a list line, returning null only for multi-char selections or non-list lines. * feat(tasks): hint "Notes / Checklist" in empty notes header When a task has no notes yet, the notes section header now reads "Notes / Checklist" instead of "Notes", hinting that the section can hold either a free-form note or a checklist. Plain notes still show "Notes" and a recognized checklist still shows "Checklist". * feat(tasks): limit checklist item click target to checkbox and text Checklist item text was rendered as a bare text node inside the block-level row, so clicking anywhere on the row (including empty space) toggled the item. Wrap the text in a <span class="checkbox-label"> and only toggle when the click lands on the checkbox icon or that label; clicks on the rest of the row fall through to opening the editor. Cursor affordance is moved off the row onto the checkbox and label accordingly. * fix(tasks): map checklist toggle to the correct source line _handleCheckboxClick mapped the Nth rendered checkbox to the Nth source line matching `line.includes('- [')`, which also matches non-task lines such as markdown link bullets (`- [text](url)`) or prose. That shifted the index so clicking a checkbox could toggle the wrong line (often a no-op). Use the anchored `isChecklistItemLine` predicate — the same one the bulk-action helpers use — so source lines align with the rendered checkboxes. Applied to both the inline editor and the fullscreen markdown dialog (duplicated logic). * refactor(tasks): unify checklist predicate and dedupe toggle logic Addresses multi-review feedback (W1/W3/S1): - Single source of truth for "is a checklist line": isMarkdownChecklist, markdownToChecklist and getChecklistProgress now use the checklist-operations predicates instead of three divergent definitions. Tightened CHECKLIST_ITEM_RE to /^\s*- \[[ xX]\]/ so it matches exactly what marked renders (verified: marked treats - [ ]/- [x]/- [X] as tasks but NOT - []). This also fixes the badge not recognizing uppercase [X] checklists. - Extracted toggleChecklistItemAtIndex() into checklist-operations and use it in both _handleCheckboxClick copies (inline editor + fullscreen dialog), removing the duplicated index-mapping + string-toggle. The helper flips only the checkbox marker (item text containing [ ] is safe) and handles [X] uncheck — both pre-existing bugs in the old inline toggle. - getChecklistProgress counts in a single pass over the lines (no intermediate markdownToChecklist array / throwaway substring allocations) — it runs on the task-card hot path. * fix(tasks): match checklist predicate exactly to marked's task rendering Final review follow-up. The line predicate now requires "- [marker] <content>" (marker box + whitespace + a non-space char), matching what marked actually renders as a checkbox — verified against marked across 15 cases. Previously an empty "- [ ] " placeholder line (or "- [x]a" with no space) was counted as an item even though marked renders no checkbox for it, which could desync the Nth-checkbox -> Nth-line mapping (e.g. "- [ ] \n- [x] real": marked shows 1 checkbox, predicate matched 2, so clicking the real item toggled the empty line). Tightening CHECKLIST_ITEM_RE/CHECKED_ITEM_RE closes that gap and makes the "in sync with marked" comment accurate. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eff41c041d
|
feat(project): project completion experience (#8036)
* feat(project): add project completion with celebration + trophy view
Completing a project marks it done and archives it, with a celebration
dialog (confetti + live stats) and a prompt to resolve unfinished tasks
(move to Inbox / mark done). Completed projects show a trophy badge and
a Reopen action on the archived-projects page.
isDone stays distinct from isArchived; selectArchivedProjects is left
intact so completed projects' tasks stay filtered out of Today/Overdue.
Append/merge deferred to #8032. Plan: docs/plans/2026-06-05-project-completion.md
* refactor(project): drop Archive menu item; Complete is the retire path
Archive and Complete produced near-identical end states; collapse to one
user-facing action. Removes the 'Archive project' menu item and handler so
Complete is the single way to retire a project. The archiveProject action,
reducer and ProjectService.archive() stay (needed for op-log replay of
historical archive ops and the legacy unarchive/restore path). Wiki updated;
menu specs cover the Complete flow.
* fix(project): keep completion dialog open for the active project
MatDialog closeOnNavigation (default true) dismissed the celebration the
moment completing the currently-active project navigated to '/'. Navigate
first, then open the dialog.
* test(project): e2e for completion flow (complete, celebrate, reopen)
Covers the resolve-unfinished-tasks prompt, the celebration dialog with
stats, the trophy badge on the archived page, and reopening. Adds an
openProjectContextMenu page-object helper.
* feat(project): confirm project completion
* feat(project): show completion celebration fullscreen
* fix(project): harden completion celebration flow
* style(project): use spacing tokens in completion dialog
* fix(project): align completion screen with project context
* style(project): soften completion screen coloring
* style(project): reuse completion screen surfaces
* fix: refine project completion dialog actions
* fix: complete projects with atomic task resolution
* fix: restore archive path in project completion UI
* refactor(project): remove dead completion code
The project-level completeProject action (OpType.Update) was never
dispatched — completion goes exclusively through the atomic
TaskSharedActions.completeProject (OpType.Batch) meta-reducer. Drop the
dead action, its reducer case, the PROJECT_COMPLETE enum member and the
immutable 'PCO' op-log code (which would otherwise be permanently
reserved for an op that can never be produced); enum count 147->146.
Also remove the unused selectCompletedProjects / selectPlainArchivedProjects
selectors (no consumers) and the misspelled, unused 'angel' confetti
field, keeping the regression tests that guard selectArchivedProjects.
* fix(project): make project completion non-reversible
Completion resolves a project's open tasks (move-to-inbox / mark-done),
which reopen cannot truly restore, so the "Reopen"/"Undo" affordances on
the completion path were misleading. Drop the celebration dialog's Reopen
button and the post-complete undo snack; the fullscreen celebration is
the feedback and deliberate reactivation still lives on the
archived-projects page (Project.reopen kept for it).
Also restore the project title param on the archive confirm dialog (it
was rendering a raw {{title}} placeholder), remove the now-unused
moveTasksToInbox / markTasksDone resolution helpers (the meta-reducer
resolves tasks atomically), and drop the orphaned UNDO / S.COMPLETED
i18n keys. Updates the completion e2e to the close flow and asserts the
resolution props are forwarded to the atomic action.
* fix(tasks): cancel native reminders for project-completed tasks
Completing a project marks its unfinished tasks done inside the
meta-reducer (no per-task updateTask), so unscheduleDoneTask$'s
native-reminder cancellation is bypassed and an OS-scheduled Android
notification could still fire for a now-done task. Add a local-only
effect that cancels native reminders for the force-completed task ids.
Local-only by design: it dispatches no actions (the persistent
dismissReminderOnly/clearDeadlineReminder would each be an extra synced
op), and done tasks are already filtered from reminders$ on all
platforms — only the native Android notification needs explicit removal.
* test(project): drop TaskService spies orphaned by helper removal
getByIdWithSubTaskData$/moveToProject/setDone/setUnDone were only used
by the removed moveTasksToInbox/markTasksDone helpers and their deleted
tests.
* feat(sync): affectedEntities multi-entity conflict detection for atomic completion
WIP checkpoint of the atomic completeProject approach. The Batch op declares
every touched entity (PROJECT, INBOX, TASKs, TODAY_TAG) via a new
affectedEntities field threaded through op-log capture, conflict detection,
the sync server (+Prisma migration) and shared-schema. Per-effect
completeProject handlers (issue two-way-sync, time-block, repeat-cfg)
re-derive the task changes the atomic op bypasses.
* revert(sync): remove affectedEntities multi-entity conflict detection
Reverts
|
||
|
|
0d1869263f |
docs: remove outdated and implemented plan docs
Delete 29 plan/design docs whose work has shipped or been superseded (SuperSync slices, sync-core extraction, encryption-at-rest drafts, document-mode/Stage-A persistence, calendar/CalDAV concepts, focus-mode time-tracking sync, etc.). Kept the still-forward-looking docs (e.g. supersync-encryption-at-rest, sync-core-simplification-roadmap, calendar-two-way-sync-technical-analysis). Source comments that cited deleted docs are rewritten into self-contained inline rationale so no "see docs/..." reference dangles. |
||
|
|
71f4ca484c
|
fix(plugins): return dialog result #5239 (#8106)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
00cd665e1a
|
feat(backup): configure automatic backup file limit #6690 (#8094)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
376e068412
|
fix(electron): respect tray title settings #7823 (#8097)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
667e8c3aa0
|
fix(azure-devops): simplify host setup #7672 (#8086)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
c0146e2307
|
feat(help): add create task how-to #8015 (#8079)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
645797f0bd
|
fix(reminder): require action for reminder popup #8051 (#8057)
* fix(reminder): require action for reminder popup #8051 * test(reminder): update deadline reminder e2e expectation --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
2ac8cf03a2
|
feat(tasks): add keyboard shortcut to set task deadlines (#8042)
* feat(tasks): add keyboard shortcut to set task deadlines * fix(shortcuts): gate togglePlay to focused tasks and fix delegation visibility * revert(i18n): remove manual translation in de.json * docs(wiki): align shortcut labels and precedence note wording * test(shortcuts): update TaskShortcutService tests to align with new precedence and delegation logic * fix(shortcut): consume events on match to prevent fall-through when task focused * test(shortcut): add regression test for ShortcutService precedence * docs(wiki): correct Schedule view shortcut label * style(i18n): remove accidental blank line in de.json * test(op-log): stabilize lock reentry regression tests with robust timeouts |
||
|
|
7c03c84fad
|
feat(history): merge Quick History and Worklog into a unified History view (#8033)
* feat(history): merge Quick History and Worklog into one view - single /history route; legacy worklog & quick-history redirect to it - one History menu entry; navigate-to-task targets history - show per-day work start-end as its own column in the day table - show enabled habits/simple-counters (icon + tooltip) in the expanded day - extract HistoryTaskRowComponent for archived task rows - remove WorklogComponent and QuickHistoryComponent * fix(history): restore quick history behavior * refactor(history): collapse to a single unified view - remove the Worklog/Quick History toggle; show one full all-time history tree (year -> month -> week -> day) - drop the quick-history mode, significance filter and ?view= param - extract shared HistoryDayMetaComponent; reuse HistoryTaskRowComponent in the Daily Summary "This Week" widget - convert project colors + dateStr auto-expand to signals - update e2e specs to the single-view selectors * refactor(history): remove dead quick-history code and i18n keys After merging Quick History into the unified History view, the quick-history data pipeline had no consumers. Remove it: - _quickHistoryData$, quickHistoryWeeks$, _loadQuickHistoryForWorkContext - map-archive-to-worklog-weeks util (+ tz spec) and WorklogYearsWithWeeks - orphaned F.QUICK_HISTORY, MH.QUICK_HISTORY, MH.WORKLOG translation keys Keep WorklogWeekSimple (base of the still-used WorklogWeek). * docs(history): reflect Quick History merge into unified History The quick-history and worklog routes are now legacy aliases for the single unified History view; drop the stale 'current-year mode/toggle' framing. Concepts-note prose flagged for human review. * feat(history): improve a11y and i18n of the unified history view Accessibility: - week table uses <thead> + <th scope=col>; icon-only work-times header gets a translated aria-label - expandable day row: real <button> disclosure control carrying aria-expanded/aria-controls (keyboard + AT path) instead of a role=button <tr>; row keeps a pointer click - archived-task title is now a <button> (was a non-focusable <span>) i18n: translate previously hardcoded aria-labels (export/restore) and the week-range tooltip; reuse VIEW_TASK_DETAILS/RESTORE_TASK_FROM_ARCHIVE, add EXPORT_DATA + WEEK_RANGE_TOOLTIP keys. Polish (carried in): convert worklogData$ to a toSignal signal (drop AsyncPipe + dead animations), relocate the shared row to features/worklog/worklog-task-row (WorklogTaskRowComponent) to break a worklog<->history cycle, make template-unused services private, tighten projectColor input type. SCSS focus rings use focus-ring tokens. e2e: locate task titles via td.title button after the span->button change. |
||
|
|
315c900391
|
fix(boards): show tasks from sidebar-hidden projects (#8021)
* fix(boards): show tasks from sidebar-hidden projects * docs(projects): clarify sidebar hidden wording * fix(nav): restore project visibility affordance |
||
|
|
ede6b770c8
|
[codex] Hide global metric charts outside Today (#8019)
* fix(metric): hide global charts outside today * refactor(metric): collapse showGlobalMetrics into isShowingAllTasks Address review feedback on the global-charts gating: - Drop the redundant showGlobalMetrics alias; it was a 1:1 pass-through of the existing computed. Make _isShowingAllTasks public as isShowingAllTasks so the template uses one source of truth (it already drives the metrics service selection and view title too). - Collapse the three repeated metricService.hasData() guards in the template into a single @if/@else, evaluating the condition once. - Fold the new Inbox coverage into the isShowingAllTasks spec and drop the now-duplicate showGlobalMetrics suite. |
||
|
|
25896fd318
|
Fix frequency translations (#8000)
* fix(translation): fall back to translation language for date/time locale * fix(translation): use correct dative ordinal inflection in German monthly nth weekday settings * fix(translation): use correct dative ordinal inflection in German monthly nth weekday settings * fix(date-time): reset date-time locale to active UI language when override is removed * fix(tasks): correct monthly repeat display precedence and validity checks * refactor(tasks): use UTC-anchored Sunday for deterministic weekday-name lookups * fix(tasks): standardize quick settings translation keys * test(tasks): improve getTaskRepeatInfoText test coverage for monthly nth-weekday and fallbacks * docs(i18n): explain dative form suffix for ORD_*_NTH translation keys * translation(de): translate all missing keys in de.json to German * fix(date-time): prevent crashes when DateAdapter is not provided or mocked * fix(tasks): use correct dative ordinal inflection in monthly nth weekday quick settings |
||
|
|
524e505353 |
fix(onboarding): harden sync setup dismissal
Mark sync-based onboarding dismissal as fully complete so the hint tour does not restart on the next boot. Guard the sync setup CTA while the dialog is loading or open, cover the async races in tests, and move the onboarding z-index to a design token. |
||
|
|
c41c18f247
|
fix(sync): guard fresh-client first sync against data-loss trap & decrypt-race (#7980)
* fix(sync): guard fresh-client first sync against data-loss trap & decrypt-race A genuinely-fresh client seeds example tasks before its first sync, so on first connect to a populated, encrypted SuperSync dataset it hit a SYNC_IMPORT conflict dialog whose USE_LOCAL would overwrite the real remote, plus a red DecryptNoPasswordError. - A1: SyncImportConflictGateService flags isNeverSynced (\!hasSyncedOps) on the incoming-import dialog; the dialog guards the destructive USE_LOCAL with a confirm and focuses the scenario-appropriate safe button via cdkFocusInitial. - B: OperationLogDownloadService logs 'encrypted ops, no key' quietly only for a genuinely-fresh client (never synced AND no local encryption flag); it stays loud (dropped-credential signature) for already-synced clients and for a wiped op store whose config still flags encryption on, via the new optional provider method SuperSync.isEncryptionEnabled(). - D: SuperSync.isReady() returns false while isEncryptionEnabled && \!encryptKey, keeping a self-inconsistent encrypted config out of auto-sync; shares the _isEncryptionHalfConfigured predicate with getEncryptKey(). Tests: super-sync isReady/isEncryptionEnabled, conflict-gate isNeverSynced, dialog confirm-guard + focus-by-scenario, download severity branches, and a sync-service end-to-end first-sync-trap guard. Plan/rationale: docs/plans/2026-06-03-fresh-client-first-sync-data-loss-trap.md * docs(sync): add fresh-client first-sync data-loss-trap plan Documents the shared root cause (example tasks seeded pre-first-sync), the A1/B/D fixes, and the accepted Fix C residual (example-task pollution onto non-encrypted accounts) with the rationale for deferring an identity-based cleanup. * fix(sync): capture never-synced guard pre-download for piggyback SYNC_IMPORT The SYNC_IMPORT conflict gate's never-synced guard (which blocks a fresh client from force-overwriting a populated remote via USE_LOCAL) was computed from a live hasSyncedOps() read on the piggyback-upload path. By the time that gate runs, the same sync has already persisted downloaded ops with syncedAt and marked accepted uploads synced, so the flag flips to "has synced" mid-cycle and the guard disarms itself — re-opening the data-loss trap on the piggyback path. Capture the never-synced snapshot once at sync-cycle start, before download, and thread it through downloadRemoteOps()/uploadPendingOps() into the gate. The gate falls back to a live read only for standalone callers (download gate is safe live: nothing is persisted until processRemoteOps). Also stop the sync-wrapper catch from re-logging the expected DecryptNoPasswordError at error level — the download/upload service already logs it at the right severity, so re-logging undid the fresh-client onboarding-prompt quieting. Tests: gate honors a caller-provided isNeverSynced without consulting live history; piggyback path flags isNeverSynced=true even when the upload marks ops synced; wrapper threads the pre-download snapshot. Refresh the plan doc's stale test counts and document the fix (§11). |
||
|
|
e0645f57c3
|
Feat/task creation due date (#7966)
* feat(tasks): add deadline support to add-task-bar * test(tasks): add unit tests for deadline creation * docs(wiki): document deadline button and shortcuts * feat(tasks): shift deadline creation to input short-syntax * test(tasks): fix expectation in mentions spec for deadline trigger * build(tasks): drop unused DialogDeadlineComponent import * fix(tasks): prevent title corruption when combining schedule and deadline * refactor(tasks): restrict preceding space guard to deadline trigger * fix(tasks): prevent hasDeadlineTime leaking onto task during short syntax edits * fix(tasks): auto-plan tasks when deadline is set to today via short syntax |
||
|
|
63d46f7a0b
|
feat(op-log): validate SQLite backend + IDB→SQLite migration + backend-aware init (#7931) (#7954)
* test(op-log): validate SqliteOpLogAdapter against a real sql.js engine The 23 adapter specs ran only against an in-memory regex stand-in that models the SQL shapes the adapter emits — it validates the translation layer, not SQLite itself. Add sql.js (dev-only; never in the app bundle) served into Karma, and run the behavioral contract against BOTH the fake and a real SQLite engine. This exercises genuine-engine behavior the stand-in could only model: the real UNIQUE-constraint message -> ConstraintError mapping, AUTOINCREMENT never reusing seq after clear(), compound-index + NULL range handling, and real BEGIN IMMEDIATE rollback. 51/51 green. B2 (translation-layer pass) per docs/sync-and-op-log/sqlite-migration.md. The integration-harness second pass and the on-device real-engine run remain. * test(op-log): run store-port integration against real sql.js (B2 stage 2) Parameterize the RemoteOperationApplyStorePort integration scenarios to run against BOTH the default IndexedDB backend and a sql.js-backed SqliteOpLogAdapter, exercising the store's COMPOSED flows (apply/mark/ merge-clock, partial-failure persistence, full-state import clearing, vector-clock persistence) on a real SQL engine — not just the adapter in isolation. 6/6 green. Surfaced a real B3 wiring gap: OperationLogStoreService.init() is IDB-shaped (opens+adopts an IndexedDB connection, never calls the adapter's own init()). For a self-managing backend like SQLite the tables would not exist. The sql.js setup creates them once on the shared db to mirror the store-init change B3 must make on native (call adapter.init() / skip the IDB open when the backend is SQLite). * feat(op-log): add verified IDB->SQLite backend migration (C1) One-time copy of the entire op-log from a source adapter (legacy IndexedDB) to a dest adapter (SQLite) in a single dest transaction with verify-before-commit: a mismatch in op count, last seq, or vector clock throws and rolls the dest back, leaving it empty and the source untouched. Adapter-agnostic (talks only to the OpLogDbAdapter port), so it is validated in CI with a real Chrome IndexedDB source + a sql.js SQLite dest; the native @capacitor-community/sqlite dest behaves identically through the same port. The generic iterate->put copy preserves ops seq (incl. gaps) via the put-honors-seq path and writes singletons at their out-of-line key uniformly, with no per-store special-casing. Not wired into startup — Phase B3/C2 decide WHEN to run it (SQLite empty + legacy SUP_OPS present) and retain the IDB copy >= 1 release. 5/5 green, incl. seq-fidelity, AUTOINCREMENT-continues-past-migrated, empty-source, non-empty-dest guard, and verify-rollback. * docs(sync): record sql.js validation, C1, and the B1/B3 findings Update the SQLite migration plan + follow-up backlog to reflect what landed this pass and hand off the device-gated remainder: - B2: real-engine (sql.js) adapter contract + store-port second pass are done in CI; only the on-device run remains. - C1: the backend-migration algorithm + verify-before-commit are done and tested (real IDB -> sql.js); only the startup wiring remains. - B3 finding: OperationLogStoreService.init() is IDB-shaped (opens+adopts IDB, never calls adapter.init()); native must call adapter.init() and skip the IDB open on SQLite. - B1 perf note: bridge round-trips dominate on native; return lastId from the plugin's run response and add a runBatch/executeSet bulk path so appendBatch is one crossing, with RETURNING-seq for per-op seq. * feat(op-log): make store init backend-aware for self-managing backends (B3) OperationLogStoreService.init() and ArchiveStoreService._init() were IDB-shaped: they unconditionally opened+adopted a WebView IndexedDB connection and never called the adapter's own init(). For a self-managing backend (SQLite) that meant (a) the adapter's tables were never created and (b) it still touched the evictable WebView store this migration exists to escape. Now: when the adapter exposes no adoptConnection (i.e. it self-manages, like SQLite), call adapter.init() and skip the IndexedDB open. The adopt-connection (IndexedDB) path is unchanged. The new branch is dead in production until B3 flips the native token, so this is risk-free now and unblocks that flip. Tested: two unit tests cover both branches (self-managing -> adapter.init, no IDB open; IDB -> open+adopt, no adapter.init). The store-port integration spec now drives the store fully on SQLite with _db undefined, so its earlier pre-init workaround is removed. 521 persistence + 6 integration green. * docs(sync): mark the B3 backend-aware init fix as landed The store-init half of B3 (call adapter.init() / skip the IDB open for self-managing backends) is implemented + CI-tested; only the device-gated native token flip + SqliteDb wrapper remain. |
||
|
|
ded0240b8e |
docs(task-repeat-cfg): revise RRULE migration plan and consolidate to one doc
Rewrite the recurring-events plan after multi-axis review against the codebase. Adopt a typed, RRULE-isomorphic recurrence model (RRULE string at the export/CalDAV boundary only), keep the synchronous occurrence engine, and route migration through the op-log schema system. Fold the gap-analysis and industry-standards research docs into the plan as appendices and remove them. |
||
|
|
64d3219d3a
|
feat(local-backup): Track A safeguards for #7925 (#7932)
* fix(android): escape JS bridge args via JSONObject.quote (#7925)
`loadFromDb` interpolated the stored value into a single-quoted JS string
literal passed to `evaluateJavascript`. Beyond the security smell, this is
a real data-loss bug: `JSON.stringify` does not escape apostrophes, so a
backup blob containing one (e.g. a task titled "don't…") terminated the JS
literal and the load returned garbage — silently corrupting any restore
from `KeyValStore`.
Use `JSONObject.quote()` (already established in the file for
`emitForegroundServiceStartFailed`) for all three callback args, so values
containing `'`, `\`, newlines or `</script>` round-trip cleanly.
* feat(startup): log storage-persistence outcome on all branches (#7925)
`_requestPersistence()` was silent on native and on the `false` resolution
of `persist()`, so #7892-style "woke up blank" reports carried no signal
about whether the WebView store was actually persistent.
Always log `{persisted, granted, isNative, isElectron}` — including the
already-persisted branch, the persist-resolved branch (both true and false),
the error branch, and the no-`navigator.storage` branch. User-facing snack
gating is unchanged (still web-only, non-onboarding). Logging-only — no
behavioral change.
This is Track A1 in `docs/sync-and-op-log/sqlite-migration-followup.md`:
the diagnostics that decide whether the deeper protective steps
(near-empty write guard, SQLite migration) are worth the added complexity.
* docs(sync): refresh sqlite-migration followup after #7924 (#7925)
The followup backlog described the local-backup ring as TODO under A2,
but #7924 already shipped the periodic + app-private backup, two-generation
ring, empty-state write guard, and informed restore prompt. Bring the doc
in sync:
- Add the shipped local-backup work to "Where we are now".
- Replace the old A2 ("Periodic local auto-backup") with the narrower
remaining gap: a debounced data-change backup trigger to complement the
5-min timer.
- Add A3 (near-empty write-time overwrite guard) with a concrete starting
threshold and a fail-safe rationale, sequenced after A1 so the
diagnostics tune the threshold before it lands.
- New "Cross-cutting / hardening" section consolidating the items
surfaced by the #7924 review (Kotlin JS-bridge escaping — now done;
backup-date reader bridge for the restore prompt; robust restore on
degraded boot; last-backup visibility; onboarding nudge for no-sync
users).
* feat(local-backup): debounced on-data-change backup trigger (#7925)
The 5-min `interval()` was the only thing that drove `LocalBackupService._backup()`,
so a destructive event in the minutes before a WebView eviction could be lost
from the backup ring even though the live store had it.
Merge a `LOCAL_ACTIONS`-driven trigger into `_triggerBackupSave$` that fires
once after a 30s quiet period. Catches the typical "user made changes then
put phone down" pattern before the next periodic tick. `LOCAL_ACTIONS`
already filters out remote/hydration replays, and the existing empty-state
guard prevents writing a degraded post-eviction snapshot over a good
backup, so this strictly adds backup frequency — never spam.
Logic-level only — `_backup()` continues to early-return on non-target
platforms (web/PWA), so the trigger is safe to subscribe everywhere.
Closes A2 (remaining gap) in
`docs/sync-and-op-log/sqlite-migration-followup.md`.
* docs(sync): mark A1 + A2 shipped in sqlite-migration followup (#7925)
A1 (storage-persistence diagnostics) and A2 (debounced data-change backup
trigger) both landed this round. Update the suggested order and the A2
section so the doc accurately reflects what's left in Track A (just A3,
the near-empty write-time overwrite guard).
* feat(local-backup): near-empty write-time overwrite guard (A3, #7925)
The exact-empty guard in `_backup()` only catches a fully-degraded store.
The residual gap is a post-eviction boot that leaves the store near-empty,
the user adds 1-2 tasks before the 5-min timer fires, and the degraded
state then overwrites the good primary slot. The prev slot is still safe,
but the informed restore prompt only fires on a wholly fresh launch — so
without this guard the user has lost direct access to the better backup
until they uninstall/reinstall.
Add a per-platform near-empty guard in `_backupAndroid` / `_backupIOS`:
read the existing primary, compare task counts (active + young-archived +
old-archived via the new shared `countAllTasks` helper), and bail when a
< 3-task snapshot would clobber a >= 10-task existing backup. Electron is
unchanged — its rotated, timestamped backup chain isn't a single-slot
overwrite.
Threshold rationale: `summarizeBackupStr` counts archived tasks too, so
"near-empty" means the same thing on the read side (the restore prompt)
and the write side (this guard). Fail-safe — skipping a write only delays
capturing a real wipe, never loses data; the guard self-clears once the
store grows back past 3 tasks, so a legitimate bulk-delete is captured
on the next tick.
Marks Track A (#7925 / sqlite-migration-followup.md) complete.
* fix(android): JSONObject.quote() the sibling JS bridge callbacks (#7925)
`saveToDbCallback` / `removeFromDbCallback` / `clearDbCallback` still raw-
interpolated `requestId` into single-quoted JS string literals. The args are
nanoid strings today so it works — but only by caller hygiene. Mirror the
`loadFromDb` fix: quote all three so the bridge contract no longer depends
on what the caller happens to pass.
Compress the `loadFromDb` rationale comment in the process — the file-level
intent now lives on one line near the cluster.
* refactor(local-backup, startup): trim Track A code per multi-agent review
Two independent reviewers flagged the same set of cleanups on the Track A
commits (#7925). Applying the high-confidence ones:
- Drop `_escapeAndroidNewlines` + its two call sites. The Kotlin bridge fix
(#7925,
|
||
|
|
4c239e5691
|
refactor(op-log): extract a swappable persistence port + SQLite backend (groundwork for #7892) (#7902)
* docs(sync): add SQLite migration plan + Phase A adapter port skeleton Documents the op-log persistence migration off WebView IndexedDB into app-private SQLite on native (Capacitor), addressing the data-loss class where Android can evict WebView storage when no sync is configured. Phase A skeleton (no behavior change, not yet wired in): - OpLogDbAdapter / OpLogTx: backend-agnostic persistence port with a callback-based transaction() as the atomicity seam both IndexedDB and SQLite map onto. - OP_LOG_DB_SCHEMA: declarative SUP_OPS schema descriptor (mirrors db-upgrade.ts v6) that both backends can consume. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * feat(op-log): add IndexedDbOpLogAdapter implementing the persistence port Phase A continuation of the SQLite migration (docs/sync-and-op-log/ sqlite-migration.md). Implements the IndexedDB backend behind the OpLogDbAdapter port: open-retry with the existing budgets, versionchange/ close re-open handling, IndexedDBOpenError wrapping, index/range queries, and a callback-based transaction() that commits on resolve and aborts on throw — the atomicity seam both backends share. Extends the port with cursor-style iterate() (continue/stop/delete/ delete-stop) to cover the latest-entry lookups and predicate pruning the store does today, plus a close() teardown hook. Spec exercises CRUD, the unique byId index, range queries, cursor direction/stop/delete, and — critically — multi-store transaction commit and rollback against fake-indexeddb. 10/10 pass. Not yet wired into OperationLogStoreService; additive scaffolding only. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): harden persistence port after multi-agent review Addresses blocking fidelity gaps found reviewing the adapter against the real store's usage, so the upcoming store refactor can be behavior- preserving: - iterate() visitor is now synchronous and receives the primary key. An async visitor could await real I/O mid-cursor, letting the IDB transaction auto-commit and the next continue() throw TransactionInactiveError. Synchronous-only also lets a buffered SQLite backend honor it without materializing the whole result set. - DbIterateOptions.query positions an index cursor at an exact key (clearFullStateOpsExcept's keyed delete). - getAll()/count() take an optional primary-key range (getOpsAfterSeq and the getUnsynced/getAppliedOpIds incremental caches use getAll(OPS, lowerBound(seq))). - getKeyFromIndex() for cheap existence probes (appendBatchSkipDuplicates' getKey, avoids deserializing the value). Tests expanded 10 -> 26: destructive clear()+delete() rollback, abort on inner-op rejection, transactional reads/index/cursor, readonly mode, keyed index iteration, compound-index match, getAll/count ranges, the close() re-open cliff, and the open-retry budgets via the _openDbOnce seam. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route import-backup methods through the persistence adapter First method group of the Phase A store migration. Adds an adoptConnection() seam so IndexedDbOpLogAdapter operates on the store's existing connection rather than opening a second one to SUP_OPS (avoiding versionchange deadlocks and doubled close/upgrade handling during the transition). The store adopts/releases the connection alongside its own _db in init()/close/versionchange. saveImportBackup / loadImportBackup / clearImportBackup / hasImportBackup now go through the adapter. Behavior is identical — same connection, same store, same keys. Verified: 170 store unit specs, 26 adapter specs, 3 archive specs, and the import-sync integration spec all green. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route state_cache + compaction methods through the adapter Second method group of the Phase A store migration. saveStateCache, loadStateCache, the migration-safety backup methods (save/load/clear/has/ restore), and the compaction counter (get/increment/reset) now go through the shared adapter. The two atomic read-modify-write methods (incrementCompactionCounter, resetCompactionCounter) use the adapter's callback transaction(), preserving their single-transaction semantics. Introduces a StateCacheEntry type; `id` is optional so the read-side return types stay assignable from the looser snapshot shapes callers construct (the pre-migration return types didn't surface `id`). Verified: 170 store unit, 53 compaction unit, 27 vector-clock, 20 compaction integration specs all green; full tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * docs(sync): track Phase A migration progress in sqlite-migration.md https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route ops-table append + markApplied through the adapter Third method group of the Phase A store migration — the higher-risk write path. append, appendBatch, appendBatchSkipDuplicates and markApplied now go through the shared adapter. The batch methods use the adapter's callback transaction() (one atomic unit, same as before); the TOCTOU-free duplicate guard uses tx.getKeyFromIndex (the byId unique index probe, issue #6343). ConstraintError->DUPLICATE and QuotaExceededError-> StorageQuotaExceededError mappings are preserved — the adapter rethrows the original DOMException so the store's catch blocks still fire. Verified: 170 store unit + 367 op-log integration specs green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route ops-table reads + full-state clears through adapter Fourth method group. getPendingRemoteOps (compound-index match expressed as a degenerate [k,k] range, with the pre-v3 fallback scan preserved), hasOp, getOpById, getOpsAfterSeq (primary-key range), the two reverse- cursor latest-full-state lookups, and clearFullStateOps/ clearFullStateOpsExcept now go through the adapter's iterate()/getAll()/ getAllFromIndex(). The keyed-index-cursor delete is factored into a _deleteOpsByIds() helper using iterate({index, query}) + delete-stop in a single atomic transaction, matching the prior behavior (no-op + no cache invalidation on empty list). Verified: 170 store unit + 367 op-log integration specs green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route unsynced/applied caches + mark methods through adapter Fifth method group. The getUnsynced/getAppliedOpIds incremental cache builds (getAll with a primary-key range), getFailedRemoteOps (compound index), markSynced/markRejected/clearUnsyncedOps/markFailed (transactional get+put loops), deleteOpsWhere (predicate cursor delete) and getLastSeq (reverse cursor reading the primary key via the iterate visitor's key arg) now go through the adapter. markFailed keeps its original behavior of NOT invalidating the unsynced cache. Verified: 170 store unit + 367 op-log integration specs green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route remaining store methods through adapter Final OperationLogStoreService group — every method now goes through the persistence adapter; no direct `this.db` calls remain. Covers hasSyncedOps (bySyncedAt index cursor), clearAllOperations, _clearAllDataForTesting (multi-store clear in one transaction), the vector-clock accessors, and the two flagship atomic flows: - appendWithVectorClockUpdate (OPS + VECTOR_CLOCK in one transaction) - runDestructiveStateReplacement (OPS + STATE_CACHE + VECTOR_CLOCK + CLIENT_ID + archive). The hand-rolled try/abort is replaced by the adapter's commit-on-resolve / abort-on-throw transaction(); success-only cache + clientId-cache invalidation now runs after the resolved transaction. The #7709 interrupt atomicity tests still pass — the adapter operates on the same adopted connection the tests spy on, so a poisoned opsStore.add still aborts and unwinds the queued clientId rotation. Verified: 170 store unit + 367 op-log integration specs (incl. the 3 clean-slate-interrupt atomicity tests) green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(op-log): route ArchiveStoreService through the persistence adapter Completes the Phase A store/archive migration. ArchiveStoreService gets its own IndexedDbOpLogAdapter that adopts its independent SUP_OPS connection (released on close/versionchange and on the iOS connection-closing retry path in _withRetryOnClose). All six accessors plus saveArchivesAtomic/_clearAllDataForTesting now go through the adapter; the dead `db` getter and its unused error constant are removed. No direct `this.db` calls remain in either persistence service. Verified: 3 archive unit + 170 store unit + 367 op-log integration specs green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * refactor(sync): fix readonly cursor regression + multi-review findings Multi-agent review of the Phase A op-log adapter (post full store/cursor migration). Addresses one live regression plus hardening; no functional behavior change. W1 (live regression fix): the migrated read-only cursor methods — getLastSeq, hasSyncedOps, getLatestFullStateOp(Entry) — ran through iterate(), which always opened a 'readwrite' transaction, so pure reads on the hot ops store took an exclusive write lock and serialized against appends (pre-migration they were 'readonly'). Add `mode` to DbIterateOptions (default 'readwrite' so delete-walks keep working) and pass `mode:'readonly'` from those four readers; clearFullStateOps* delete-walks stay readwrite. W2: op-log-db-schema reuses DB_NAME/DB_VERSION from db-keys.const instead of re-literaling 'SUP_OPS'/6 (no third source of truth), and a new op-log-db-schema.spec.ts asserts the descriptor matches both DB_VERSION and the stores/indexes runDbUpgrade actually creates (the contract Phase B builds on). W3: test the adoptConnection seam (both branches) — ops route onto an adopted external connection, and adoptConnection(undefined) returns to the not-initialized cliff (the store's close/versionchange path). W4: convert the two open-retry specs from real ~8s backoff sleeps to fakeAsync + tick (adapter spec ~0.04s vs ~8s) and assert exact attempt budgets; add a full-lock-budget case. Gates: adapter 30 + schema 2 + store 170 unit, and 59 op-log integration specs (race-conditions, multi-entity-atomicity, compaction, server-migration, clean-slate-interrupt, indexeddb-error-recovery) green; checkFile clean. * refactor(op-log): inject the persistence adapter via DI token Phase B step 1. Both persistence services now obtain their OpLogDbAdapter from OP_LOG_DB_ADAPTER_FACTORY instead of constructing IndexedDbOpLogAdapter directly. The token vends a factory (not a singleton) because each service adopts its own connection into its own adapter instance. Defaults to IndexedDB on all platforms; Phase B step 2 will override it to return a SqliteOpLogAdapter when running native, with the stores untouched. adoptConnection() becomes an optional bridge method on the OpLogDbAdapter interface — documented as IDB-transition-only; a self-managing backend (SQLite) leaves it undefined and callers guard with `?.()`. Verified: 170 store unit + 3 archive unit + 367 op-log integration specs green; tsc clean. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * feat(op-log): add SqliteOpLogAdapter skeleton (Phase B, no native dep) Dependency-free skeleton of the SQLite backend behind the OpLogDbAdapter port. Solves the hard schema-mapping question the reviewers flagged without pulling in a native plugin or anything untestable in CI: - planTables()/buildDdl(): derive the physical SQL layout from the shared OP_LOG_DB_SCHEMA. Each store -> a table with a JSON `value` column plus one extracted column per IDB index. ops gets `seq INTEGER PRIMARY KEY AUTOINCREMENT` (monotonic, never-reused — matches IDB + getLastSeq), `op_id TEXT UNIQUE` (byId), `synced_at` (bySyncedAt) and a composite (source, application_status) index. keyPath stores -> TEXT PK from the keyPath; keyless singletons -> caller-supplied TEXT key. - A minimal SqliteDb port (run/query) the adapter talks to instead of importing @capacitor-community/sqlite, so this file has no native dependency and is unit-testable with a fake. - init() applies the DDL (idempotent); query/tx methods throw a loud not-implemented error (fail loudly rather than silently lose data) with the intended SQL documented per method. adoptConnection is intentionally absent — SQLite self-manages its handle. 12 specs cover the plan/DDL derivation and that init() emits the expected DDL. Doc updated with status + the deferred native-dependency decision. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * feat(op-log): fully implement SqliteOpLogAdapter (still no native dep) Completes the SQLite backend behind the OpLogDbAdapter port. All query, index, range, count, cursor-iterate and transaction methods are now implemented against the minimal SqliteDb port: - value→column extraction: each store row stores the JSON object in a `value` column plus extracted columns for the indexed paths (op_id/synced_at/source/application_status); writes populate them. - transactions map to BEGIN IMMEDIATE/COMMIT/ROLLBACK with rollback-on- throw; readonly iterate/transaction use no write lock. - SQLite errors map to the SAME DOMException names the store's existing catch blocks expect: UNIQUE→ConstraintError (→DUPLICATE_OPERATION_ERROR), disk-full→QuotaExceededError (→StorageQuotaExceededError). - ops uses AUTOINCREMENT so seq is monotonic and never reused across clear() — matching IDB + getLastSeq. Still imports no native plugin: a thin wrapper over @capacitor-community/sqlite's SQLiteDBConnection will satisfy SqliteDb on device. 23 specs validate the translation layer + transaction semantics (commit/rollback/abort-on-unique) against an in-memory SQLite stand-in; a real-engine on-device run is the remaining Phase B step (documented). https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * docs(sync): add SQLite migration follow-up backlog Actionable, ordered backlog companion to sqlite-migration.md: - Track A: ship the #7892 safeguards now (persist() diagnostics + native filesystem auto-backup) — independent of SQLite, recommended near-term fix. - Track B: finish the native SQLite backend (plugin + SqliteDb wrapper, real-engine validation, DI flip behind a flag). - Track C: one-time IDB→SQLite data migration, staged rollout. - Track D: cleanup once SQLite is the native default. https://claude.ai/code/session_011wcqZgubKqoT6wxt1L1KBT * fix(op-log): scan full-state ops read-only to drop the write lock clearFullStateOps / clearFullStateOpsExcept iterate the ops store only to collect ids (the delete runs in a separate transaction), but the migrated iterate() defaulted to 'readwrite' — so these pure-read scans took an exclusive write lock on the hot ops store and serialized against appends. Pre-adapter (master) these scans used a readonly cursor. Pass mode:'readonly' to restore parity. Same regression class the earlier W1 fix addressed for getLastSeq/hasSyncedOps/getLatestFullStateOp(Entry); these two scans were missed because they are no longer delete-walks. Verified: 170 store unit + server-migration/import-sync/remote-apply/ vector-clock-import integration specs green; checkFile + tsc clean. * fix(op-log): correct SQLite seq round-trip + enforce tx scope and readonly Hardens the dormant SQLiteOpLogAdapter against three multi-review findings (translation-layer only; the backend is still wired to nothing): - C1 (data duplication): the autoinc `ops` PK (`seq`) lived only in its own column, never the JSON value, and was never re-injected on read. So reads returned seq===undefined and put() emitted INSERT…ON CONFLICT(seq) with no seq bound — the conflict never fired and every mark*/clearUnsynced re-put inserted a duplicate row. Now buildInsert binds seq when the value carries one (re-put / explicit-seq add) and decodeRow injects the PK back from a `__pk` alias on every read, matching IDB's keyPath+autoIncrement store. ON CONFLICT no longer overwrites the PK column. - W2 (atomicity scope): transaction() discarded its `stores` argument, so the OpLogTx could touch any store — silently passing where IDB throws. The tx now enforces the declared scope (and inherits the tx mode for iterate). - W3 (readonly contract): a delete action under a readonly iterate executed the DELETE outside any transaction; it now rejects with ReadOnlyError, matching IDB. Also makes the in-memory FakeSqliteDb faithfully model AUTOINCREMENT (honor an explicit seq, upsert on PK conflict, advance the high-water mark) so the spec actually catches C1-class bugs — verified: reverting the seq fix makes the new "updates in place" test fail with the real UNIQUE violation. Verified: 27 SQLite adapter specs (4 new) green; checkFile + tsc clean. * refactor(op-log): strip the autoinc keyPath prefix via extractPath idiom Follow-up review nit: decodeRow stripped the `$.` from the autoinc keyPath with slice(2); use the same `.replace(/^\$\./, '')` idiom as extractPath for consistency, and note that the autoinc keyPath is a top-level field. No behavior change (keyJsonPath is always `$.seq` for the only autoinc store). --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
91a407346d
|
docs(caldav): mark VEVENT two-way as shipped via the bundled plugin (#7879)
The CalDAV VEVENT expansion design doc still says 'Status: Planned', but two-way VEVENT sync + task->event time-blocking shipped via the bundled CalDAV Events plugin (packages/plugin-dev/caldav-calendar-provider), not by extending the core CalDAV provider as the doc proposed. The core provider remains VTODO-only. Update the status banner + add an as-built note; retain the historical design. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dc3523b1c7
|
ci: auto-submit iOS and macOS App Store builds for review (#7857)
* ci: auto-submit iOS and macOS App Store builds for review The iOS and Mac App Store workflows previously stopped after uploading the build to App Store Connect via altool, leaving version creation, "What's New" and submission as manual steps. Add fastlane lanes (ios/mac release) that upload the prebuilt .ipa / MAS .pkg using App Store Connect API key auth (reusing the existing notarization key secrets), push release notes derived from build/release-notes.md, wait for processing, submit for review and flag automatic release on approval. Final version tags submit for review; pre-release tags (RC/beta/alpha) and manual runs only upload the build. Listing metadata and screenshots remain curated by hand in App Store Connect. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * ci: wire iOS and Mac Store workflows to fastlane submit lane The previous commit added the fastlane lanes but the workflow edits were not applied. Replace the altool validate/upload steps in the iOS and Mac App Store workflows with the fastlane submit lane: install fastlane, generate the App Store "What's New" notes and run `fastlane <platform> release` with App Store Connect API key auth. Also extend the Mac workflow's harden-runner egress allowlist with the rubygems and App Store Connect endpoints used by fastlane. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): harden Apple App Store auto-submission after review Address review findings on the iOS/macOS App Store automation: - Tag gating: submit only when the tag has no "-" (final semver), instead of denylisting RC/beta/alpha. GitHub Actions contains() is case-sensitive and the repo's RC tags are mostly lowercase (-rc.N), so the old guard would have auto-submitted release candidates to production review. - Fastfile: set skip_metadata so deliver no longer reads back and re-uploads curated listing fields; push only "What's New" via an inline release_notes hash. Warn against verbose mode (can dump the API key). - Gemfile.lock: add arm64-darwin/x86_64-darwin platforms so bundle install works on the macOS runners. - Workflows: install deps via pinned ruby/setup-ruby (bundler cache), and resolve the artifact path with a strict nullglob check (exactly one match) instead of ls | head. - release-notes script: tighten emphasis regexes so stray * / _ (globs, snake_case) survive, anchor footer patterns so legitimate "download" lines are not dropped, and drop a duplicate mkdir. - Docs: document the hyphen-based gate, single-use build numbers, automatic release behavior and inline validation. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): correct deliver metadata + setup-ruby version (second review pass) Two bugs introduced by the previous review-fix commit, both confirmed against upstream source: - Fastfile: skip_metadata: true makes deliver's upload_metadata return early (verified in fastlane 2.225.0 deliver/lib/deliver/upload_metadata.rb), so the "What's New" notes were never uploaded. Revert to metadata_path pointing at a dir that contains only <locale>/release_notes.txt; load_from_filesystem reads only that file (next unless File.exist?) with no remote read-back, so other listing fields stay untouched. Removed the now-unused inline release_notes helper. - Workflows: ruby/setup-ruby throws when ruby-version is unset and no .ruby-version file exists (it does not fall back to system Ruby). Pin ruby-version: '3.3' in both workflows. Docs updated to match the corrected metadata approach. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): remove invalid wait_for_uploaded_build from deliver lanes wait_for_uploaded_build is a pilot/upload_to_testflight option, not a deliver one. Passing it to upload_to_app_store makes fastlane raise on the unknown key and fail both iOS and macOS release lanes on every run. deliver already waits for the build to finish processing during submit (select_build -> wait_for_build_processing_to_be_complete), so no replacement is needed. Also pin the ruby/setup-ruby comment to its resolved version (v1.310.0), and slice the App Store release notes by code point so a multi-byte character is never split at the 4000-char cap. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5fa1383bc5
|
fix(ios): reconcile time tracking and focus mode on app resume (#7837)
* fix(ios): reconcile time tracking and focus mode on app resume iOS suspends the WKWebView WebContent process within seconds of backgrounding, freezing JS timers so tracked time and the focus-mode countdown stall. On resume we now credit the wall-clock gap (capped at MOBILE_BACKGROUND_IDLE_CAP_MS) via a wake-up tick, reset the tracking anchor, flush accumulated time, and nudge the focus reducer to recompute elapsed. On pause we flush accumulated time and drain the op-log write queue inside the existing BackgroundTask.beforeExit budget. Effects are gated by IS_IOS_NATIVE and registered only on iOS; handler bodies are exported as pure functions for unit coverage. Refs #7824, #7826 * fix(ios): persist tracked time in background budget, drop duplicate flush The flushOnPause$ effect duplicated the op-log drain that main.ts already runs inside BackgroundTask.beforeExit, but executed outside that budget and raced the main.ts listener — so accumulated time dispatched by the effect could be lost on suspension. flushPendingWrites also has a 30s timeout, well beyond the iOS budget, so the "drains within budget" premise was false. Move the only needed pause work — flushAccumulatedTimeSpent() — into the existing budgeted main.ts iOS handler before the drain, and remove the pause effect, its OperationWriteFlushService dependency, and onPause$. onResume$ becomes a plain Subject since the iOS producer is a JS listener registered at bootstrap (no cold-start replay race, unlike the native-fed Android shim). Refs #7824, #7826 * test(ios): cover resume effect wiring via extracted factory The reconcileOnResume$ effect field is false under Karma (IS_IOS_NATIVE gate), so the onResume$ -> withLatestFrom(selectTimer) -> handler pipe was untested. Extract the pipe into an exported reconcileOnResume factory and add specs that drive it against a mock store, covering the selector read and per-resume reconciliation that the pure-handler tests could not reach. Refs #7824, #7826 |
||
|
|
f10d0e9d48
|
Android soft-keyboard: fix dark-theme white flash on resize (#7839)
* test(e2e): raise timeouts for two CI-flaky waits Both timeouts repeatedly hit their limit on saturated scheduled runners without indicating a real product regression: - supersync.page.ts:781 — final sync-state icon waitFor went 10s → 30s. The race above it already consumed a 30s budget, so falling back to 10s here is too tight when the runner is hot (e.g. SuperSync 5/6 in run 26514574130, "Client A can migrate multiple times" failure). - repeat-task-day-change #6230 — post-midnight visibility went 30s → 60s. Even with the focus-event fallback added in |
||
|
|
9a7a86c8ec
|
Feature plugins app state (#7803)
* chore(plugin-api): normalize index exports * feat(plugin-api): expose app state types and getAppState * feat(plugins): expose getAppState through PluginAPI * feat(plugins): expose getAppState in plugin-bridge * fix(plugins): restore tag selector import * test(plugins): cover PluginAPI.getAppState * fix(plugins): expose getAppState for iframe PluginAPI * docs(plugin-api): add getAppState permission + example * docs(plugins): concise getAppState entry * fix(plugins): redact credentials from getAppState snapshot `getAppState` previously returned `globalConfig` and `projects[]` verbatim, exposing per-installation secrets to every loaded plugin. Strip the three known credential surfaces before returning: - `globalConfig.sync` — WebDAV/Nextcloud passwords, SuperSync access tokens, encryption keys - `globalConfig.misc.unsplashApiKey` — user-supplied API key - per-project `issueIntegrationCfgs` — Jira/CalDAV passwords, GitLab/Redmine tokens, OpenProject/Trello/Linear keys Add a security-regression spec that seeds sentinel credential strings into each surface and asserts none appear in `JSON.stringify(snapshot)`. --------- Co-authored-by: 00sapo <00sapo@noreply.codeberg.org> Co-authored-by: Johannes Millan <johannes.millan@gmail.com> |
||
|
|
6d81dde043
|
feat(plugins): adopt PERSISTED_DATA_CHANGED in document-mode (#7752) (#7812)
* feat(plugins): adopt PERSISTED_DATA_CHANGED in document-mode (#7752) Document-mode's iframe editor went stale when another device edited the same context's doc — the editor kept typing on in-memory `storedState` and the next throttled save merged-on-stale-base, partially clobbering the remote edit. Background's `enabledIds` set had the same gap for remote toggles. Adopts the host-side `PERSISTED_DATA_CHANGED` hook (shipped in #7805, made multi-handler-safe in #7811) in both surfaces: - `background.ts` — on fire: re-read `enabledIds`, diff, and call `showInWorkContext`/`closeWorkContextView` only when the active context's membership flipped. Idempotent on no-op re-fires; the hook also fires for `doc:` writes which background ignores via the set-unchanged short-circuit. - `ui/editor.ts` — on fire: load the current ctx's raw `doc:` bytes via the new `loadContextDoc` `{ raw, parsed }` shape, byte-compare to `lastSeenRemoteData`. Equal → noop (self-echo or another key's fire). Different → show a one-button "reload" banner with a distinct `doc-banner--remote-update` modifier class. Clicking Reload re-runs `setActiveContext` which already re-reads + setContent's. `isDocCorrupt === true` short-circuits to a direct reload (the corruption banner already promises saved data is untouched). The load-bearing piece is `lastSeenRemoteData` — captured as the raw `loadSyncedData()` string on both load (`setActiveContext`) and write (`flushSave` / `flushSaveSync`). The editor's own `JSON.stringify(getJSON())` is NOT byte-stable across the load path because `prepareStoredDoc` reshapes chip content against the local task cache, so comparing against editor output would mis-flag every load as remote. Raw-against-raw keeps the host's deterministic encoding the only thing that matters. Acceptance criteria from #7752: all met except the E2E append, which needs iframe-level state injection that the existing spec helpers don't cover; covered by manual verification + unit specs instead. What this doesn't fix: - No cross-context notification (editor catches up silently on switch). - Same-context concurrent edits still resolve whole-doc LWW. - No silent swap or selection preservation. - No "Keep mine" force-flush — dismiss-and-keep-typing already wins LWW. Tests: 84/84 plugin specs pass (+6 background hook membership branches, +1 corrupt-entry raw-bytes case, +1 saveContextDoc-returns-raw case). Plan v6 changelog documents the v5/v6 multi-review iterations and the cuts (pre-reload backup, pure-fn file, coalesce timer, "Keep mine"). * refactor(plugins): apply doc-mode review findings (#7752) Multi-review of the prior commit surfaced two real and one likely-real bugs in the reconciler and a few cleanup wins. Apply all that survived verification. W1 — wrap getActiveWorkContext in try/catch. The prior handler only caught loadEnabledCtxIds rejections; a getActiveWorkContext throw would escape via the void onPersistedDataChanged() registration as an unhandled rejection and leave enabledIds stale across every subsequent fire. W2 — snapshot enabledIds at entry. The prior handler read the closure variable across multiple awaits before assigning at the end; two interleaved fires could mis-compute wasActiveEnabled and drop a close/show call. Pass-by-parameter to reconcileEnabledIds eliminates the in-function race; concurrent fires now race only on the final caller assignment, which is at least eventual-consistent. W3 — extract reconcileEnabledIds to its own file and import the real function from the spec (no more local copy). reconcile-enabled.ts sidesteps the testability problem at its root: background.ts has top-level PluginAPI side-effects (registerWorkContextHeaderButton, registerHook) that crash in node, so the spec can't import from there. The new file holds only the pure reconciler + try/catch hardening. W4 — extract serializeContextDoc(doc) helper. flushSave and flushSaveSync now produce byte-identical output via the same function, so a future encoding change can't silently desync the sync path from the async one. W5 — fix the inline comment on showRemoteUpdateBanner re-entrancy to match what the code actually does (early-return because text is invariant, not "replace text in place" as the v6 plan implied). S1 — rename lastSeenRemoteData → lastSeenDocBytes. The variable also holds local-write bytes, not only "remote" ones; the new name follows the existing lastSeenTaskIds convention. S2 — drop the try/catch around loadSyncedData in onRemotePersistedDataChanged. The hook dispatcher already catches + logs handler rejections (PluginHooksService._invokeWithTimeout), and loadContextDoc elsewhere in this file follows the no-wrap convention. S3 — add primitive-JSON case to persistence.spec.ts. loadContextDoc's new {raw, parsed} shape silently forwards parsed=123/null/"hi" to callers; the editor's truthy guard is the safety net, but the persistence contract is now locked. S5 — explain why isDocCorrupt short-circuits to a direct reload (auto-recovery without user click; alternative would leave the user stuck on the corruption banner even when the remote already fixed the entry). Tests: 86/86 plugin specs pass (+7 reconciler error/membership cases, +1 persistence primitive-JSON). Bundle redeployed. * docs(plugins): update stale lastSeenRemoteData refs in JSDoc/comments Two comments referenced the pre-rename variable name. |
||
|
|
3c69aa961e
|
feat(plugins): fire PERSISTED_DATA_CHANGED hook on persisted-data changes (#7805)
* feat(plugins): fire PERSISTED_DATA_CHANGED hook on persisted-data changes
Wires the dead PluginHooks.PERSISTED_DATA_UPDATE enum into a fired hook,
renamed PERSISTED_DATA_CHANGED. Plugins are notified when their persisted
data changes for any reason after the host's initial boot load — local
writes, remote incremental sync via bulkApplyOperations, and post-boot
wholesale loadAllData paths (SYNC_IMPORT / BACKUP_IMPORT / validation
repair / recovery).
Selector-based effect on selectPluginUserDataFeatureState, gated on
SyncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$ so the
boot-time state seeds the pairwise baseline. Differ compares prev/next
by === on the encoded data blob (never decoded). Stage A composite
entityIds (pluginId:key) are normalized to owner pluginId and deduped
so a plugin with N keyed entries changing in one emission fires exactly
once. Per-pluginId dispatch via new PluginHooksService.dispatchHookToPlugin.
Effect is { dispatch: false } and creates no ops, so no sync-window
guard is needed — and adding one (skipDuringSyncWindow) would silently
suppress the very remote-sync deliveries the hook is designed to catch.
Closes #7754. Follow-up: doc-mode adoption tracked in #7752.
* test(plugins): tighten PERSISTED_DATA_CHANGED spec + docs
Multi-review pass 2 surfaced that the 5s timeout spec asserted only that
the dispatcher resolved — it would have silently passed if the timeout
race were removed entirely. Spy on PluginLog.err and assert the timeout
message actually reached the catch branch.
Also:
- Add `:` guard to PluginHooksService.registerHookHandler so the
persistence-key grammar is enforced at both the persistence and
hooks-registry endpoints (defense-in-depth; composeId already throws
at the bridge).
- Add async-rejected-promise spec to cover the Promise.race branch that
sync-throw didn't exercise.
- Carry the PERSISTED_DATA_CHANGED contract paragraph into
docs/plugin-development.md and docs/wiki/3.01-API.md (previously only
in packages/plugin-api/README.md).
- Clarify loadSyncedData(key?) in the README for keyed plugins.
|
||
|
|
8f274582e2
|
feat(plugins): Stage A keyed persistence with LWW (#7749) (#7763)
* feat(plugins): keyed persistence API for per-context LWW (Stage A Phase 1+3) Add an optional `key` argument to `persistDataSynced` / `loadSyncedData`, composed at the bridge transport boundary into `pluginId:key` entity ids. Distinct keys now produce distinct ops that LWW-resolve per-entity, enabling document-mode-style plugins to avoid cross-context blob overwrites without changing existing keyless callers. Phase 3: `removePluginUserData(pluginId)` now sweeps the full prefix (legacy entry + every keyed entry), dispatching one delete per match with the rule-6 setTimeout(0) trailer so remote replicas don't keep keyed entries after uninstall. The reducer-only "smart prefix match" shortcut is wrong (one op for the prefix only, remote keyed entries leak) — see docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md Phase 3. Phase 4 (document-mode plugin-side migration of the legacy single-blob entry) is left as a separate follow-up so the host change can be reviewed in isolation. Issue #7749 * feat(document-mode): migrate to keyed persistence (Stage A Phase 4) Move from one synced blob under the bare plugin id to per-entity keyed entries: - meta — { enabledCtxIds: string[] }, owned by background.ts - doc:${ctxId} — one entry per context, owned by the editor iframe - __meta__ — migration stamp Each entry has its own LWW timestamp on the host, so a concurrent edit in project A on Device 1 and project B on Device 2 no longer whole-blob-collide. The migration runs idempotently from both background.ts and editor.ts (stamp-guarded), splits the legacy single blob into keyed entries, then tombstones the legacy entry with an empty payload — giving LWW a winning side against any offline device that still writes the old shape. flushSave / flushSaveSync no longer need to read+merge sibling state, since each context's entry stands alone. The future-version blob guard (isStorageUnreadable) is dropped — it referenced the wrapping blob's version, which no longer exists; per-doc corruption still falls back via isDocCorrupt. Issue #7749 * fix(plugins): tighten Stage A keyspace at the boundaries Multi-review surfaced three small gaps in the keyed-persistence rollout: - The synchronous composeId throw covers the bridge's iframe and direct-API entry points, but three in-process callers (plugin-config.service, plugin.service, plugin-config-dialog) bypass the bridge and route directly into the persistence service. A user-installed plugin with `id: "evil:plugin"` passed manifest validation and would have collided with the legitimate `evil` plugin's keyed namespace — `removePluginUserData('evil')` would have over-matched the sweep. Reject the colon at install time in `validatePluginManifest`; keep the bridge throw as defense-in-depth. - The new `key` arg at the bridge was typia-asserted on `data` but unchecked itself. A compromised iframe could pass a multi-megabyte string or a non-string value via postMessage. `data` is capped at 1 MB, but the entity id composed from `key` would be stored verbatim in NgRx state, IndexedDB, the op-log, and on the sync wire — bypassing the data cap. Add `assertPluginPersistenceKey` with a 256-char cap. - `_loadPersistedData` silently returned `null` when composeId threw, while `_persistDataSynced` rethrew. The asymmetry made a malformed pluginId look like "no data yet" on the load side, indistinguishable from a fresh install. Hoist composeId + key validation out of the load try/catch so it throws symmetrically. Issue #7749 * fix(plugins): lower per-write cap to 256 KB The pre-Stage-A 1 MB cap was sized for the old single-blob shape, where one entry held every context's data. With the keyed split, each entity gets its own write budget — 1 MB per write is wildly over-provisioned for the realistic upper bound of plugin payloads (heavy document-mode docs ~30–100 KB, configs and automations KB-scale). 256 KB keeps 2–5× headroom over realistic payloads while bounding the per-plugin storage growth more tightly. Document-mode's migration loop now skips oversized legacy docs instead of aborting the whole run: a user whose legacy blob holds one ~500 KB doc (legal under the old cap) keeps the other contexts migrated and the original bytes preserved in the legacy entry. The success stamp stays at migrated:0 in that case so a future build (or pruning of the doc) can complete the migration without data loss. Issue #7749 * test(plugins): e2e migration of legacy single-blob to keyed entries The migration logic in document-mode is unit-tested against a mock PluginAPI, which can't catch real-iframe quirks (postMessage handling of undefined second args, commit-chain timing under the host's per-entity rate limiter, hydration ordering against the op-log). Add two end-to-end scenarios: - Fresh install: enable the plugin, verify the __meta__ stamp lands at migrated:1 (the migration's final write — observing it implies every earlier step completed). - Legacy blob: seed a pre-Stage-A single-blob entry via the e2e helper store, enable the plugin, verify the legacy entry is tombstoned, meta carries the enabledCtxIds, and each doc landed under its own doc:${ctxId} key. Issue #7749 * chore(plugins): drop dead code and review-driven polish Four small follow-ups from the multi-review pass: - Don't log the plugin-supplied `key` value. Plugins may use user content (search queries, doc titles) as keys; the log history is exportable. Log `keyLen` instead, per CLAUDE.md rule 9. - Delete `detectStaleLegacyWrite` and its 3 specs. Exported and fully tested, but zero non-test callers — banner UI is forbidden by project convention for transient-only messaging. If the need resurfaces, the implementation is four lines. - Drop the `attemptedAt` field from `MigrationStamp`. It was written but never read; the success stamp is the only re-entry gate, and the resume path is just "re-run the loop" — re-writes are content- idempotent. Saves one rate-limited write per fresh migration. - Update `docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md` with an implementation-status table referencing the shipping commits, so future readers don't have to dig through git. Issue #7749 |
||
|
|
196e50b906
|
test: strengthen unit test assertions and revive disabled plugin specs (#7755)
* chore(plugins): re-bundle document-mode and document Stage A path Reverts the unbundling from |
||
|
|
0c2420eeb3 |
feat(plugin): improve sync data size for plugins
- docs(plugins): remove delta-sync plan; track Stage A as GH issue - fix(plugins): preserve teardown safety net + earlier base64 size gate - fix(plugins): address second-round multi-review findings - refactor(sync): drop unused isVirtualEntity host re-export - fix(plugins): address multi-review findings in plugin persistence - perf(plugins): delegate plugin-data codec to sync-core helpers, cap decompression - docs(sync): mark delta-sync plan implementation status - perf(plugins): gzip plugin user data at the persistence boundary - perf(document-mode): raise save throttle and add focus-loss flush triggers - fix(sync): resolve plugin-data LWW conflicts via array storagePattern |
||
|
|
34af7b1036 |
feat(projects): polish archive flow per #7748
- Confirm dialog for archive (replaces snack+UNDO); navigation in work-context-menu now awaits confirmation. - Archived-projects rows link to /project/<id>/tasks; project view shows an inline restore notice when the active project is archived. - Unarchive snack split: when the project is still hidden from the menu, offer a "Show in menu" action; otherwise keep UNDO. - Terminology: snack now reads "Project restored" to match the button. - Tests: spec for archived-projects-page and project-task-page; defensive archive-filter coverage on selectAllTasksWithReminder / selectAllTasksWithDeadlineReminder / selectOverdueTasks; service spec covers confirm/cancel paths and the hidden-from-menu branch. |
||
|
|
b945091b83
|
Archive project - Continuation of #7162 (#7639)
* feat(project): add archive project UI Allow users to archive completed projects via the project context menu. Archived projects are hidden from the sidebar and visibility menu, but retained with all their tasks. They can be unarchived at any time from the visibility menu (eye icon), where they appear below a divider with an unarchive icon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(project): add unit tests for archive/unarchive project feature - projectReducer: verify archiveProject sets isArchived=true, unarchiveProject sets it back to false, and that other projects are not affected - WorkContextMenuComponent: verify archiveProject() calls the service, shows a snack, navigates away when the archived project is currently active, and does not navigate otherwise Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(project): document archive/unarchive project feature Add "Archiving Projects" section to the Project View wiki page explaining how to archive a completed project and how to restore it via the visibility menu. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(project): new archived projects page * feat(project): improve archive UX with undo snack and hidden-menu warning - Replace plain archive notification with icon + undo action so accidental archiving is recoverable without a confirmation dialog - Add snack on unarchive; warn when project is still hidden from menu (isHiddenFromMenu=true) so users know why it won't appear in sidebar * feat(reminder): suppress reminders for archived project tasks * feat(tasks): hide archived project tasks from tags, today, and boards * feat(tasks): hide archived project tasks from overdue tasks list * fix(repeatTasks): exclude TaskRepeatCfg of archived projects when generating the next instance * fix(tasks): hide Task and TaskRepeatCfg of archived project from the /scheduled-list page This effectively hides the "Overlaps with another scheduled task" message if the other task belongs to an archived project. * feat(docs): Add documentation for archived projects * fix: revert changes to the it.json file * feat: hide all task-repeat-cfg from planners and introduce a new task-repeat-cfg selector selectActiveTaskRepeatCfgs * feat(tasks): add base selector for filtering tasks from active projects * feat(ui): add count of archived project in project visibility menu and hide the archived projects page link when none exist * feat(ui): improve accessibility for archived projects page by adding aria-labels and aria-hidden attributes * perf(projects): use a sorted selector in archived projects page to avoid sorting during user's search * refactor(tasks): derive selectAllUndoneTasksWithDueDay from the common shared selector * refactor(tasks): introduced shared selectors to centralize active projects filtering * fix: add mock selector for selectUndoneOverdueDeadlineTasks in plan view tests * refactor(planner): use selectAllTasksInActiveProjects to exclude archived project tasks Replaces direct task state access with the shared selectAllTasksInActiveProjects selector, which already filters archived project tasks. Fixes archived project tasks appearing in planner view. Test mock stores updated to include tasks+projects slices (required by new selector chain). Removed "missing entity references" test: that guard now lives upstream in selectAllTasks. * refactor(work-context): exclude archived project tasks from TAG/TODAY and SCHEDULE selectActiveWorkContext and selectTodayTaskIds were using raw taskState.entities, causing archived-project tasks to appear in tag boards and Today. Fix by filtering entities in the selectors using selectArchivedProjectIds before calling computeOrderedTaskIdsForToday / computeOrderedTaskIdsForTag. Apply the same logic to selectTimelineTasks and remove the combineLatest workaround added in 9d6fe5e. * fix(work-context): exclude archived project's tasks when selecting tasks in schedule page * fix(tasks): exclude archived project tasks from unplanned deadline banner * fix: fix tests after rebase onto master * fix: stabilize selectArchivedProjectIds Set instance Derive selectArchivedProjectIds from a base selectArrayOfArchivedProjectIds selector to avoid recreating the Set whenever unrelated project properties change * perf(selectors): avoid unnecessary recomputations from archived project selectors selectArchivedProjectIds was creating a new Set on every project-state update, causing unnecessary recomputation across downstream selectors even when archived projects did not change. - Add selectArrayOfArchivedProjectIds as a stable intermediate selector - Rebuild selectArchivedProjectIds only when archived IDs change - Memoize active-project task entities instead of filtering inline - Remove unused selectTaskFeatureState dependency from selectActiveWorkContext * fix(project): prevent inbox project to be archived * fix(task): add filters for non-archived projects and comment explaining why filter was not added in selectors * fix: always call archive service before navigateByUrl when triggering archive action * feat(ui): add undo action on project restored from archive snack * fix: add guards against empty projectId in tasks * fix(tasks): keep orphaned subtasks scheduled in selectLaterTodayTasksWithSubTasks Restored selector to its original form, but reading tasks from selectAllTasksInActiveProjects instead of selectTaskFeatureState. While categorizing tasks, group subtasks by their parentId in a single pass to avoid calling mapSubTasksToTask that requires TaskState. * refactor(tasks): compose selectAllTasksWithoutHiddenProjects from selectAllTasksInActiveProjects + new selector for hidden projects * test(tasks): restore coverage gaps from selector refactors - selectAllUndoneTasksWithDueDay: add "should include subtasks with dueDay" (dropped when selectAllTasksWithDueDay was removed in |
||
|
|
7a93265281 |
docs(sync): add document-mode sync data model and delta-sync plans
Two design docs for slimming the document-mode plugin's sync footprint: the sync-data-model plan covers the immediate fix (bare-atom chips) plus deferred per-context entities; the delta-sync plan analyses why true deltas need finer entity granularity or a commutative CRDT (Yjs) given the partially-ordered op-log. Refs #7740. |
||
|
|
713245e6f0 |
docs(sync): add SUP_OPS versionchange handler plan (#7735)
Rescopes #7735 from the original three-connection consolidation down to its one genuine correctness fix: register versionchange handlers on the OperationLogStoreService and ArchiveStoreService SUP_OPS connections so a future schema bump cannot be stalled by a handler-less connection. The connection consolidation is documented as not planned (no behavioral benefit, net-additive LOC on a safety-critical path); the rejected "OperationLogStoreService as connection owner" alternative is kept for the record. Both decisions followed multi-agent review rounds. |
||
|
|
50e2b53d90 | Merge branch 'master' into feat/doc-mode4-2880bb | ||
|
|
508998c6a1
|
Improve on sync (#7736)
* fix(android): restore share title derivation and dedupe shared tasks Commit |
||
|
|
1c10ff67dd |
feat(document-mode): add TipTap-based document-mode plugin
A document-mode plugin under packages/plugin-dev/document-mode/ that renders the active work context's tasks as an editable TipTap document. Per-context doc state is persisted as a last-writer-wins JSON blob via PluginAPI.persistDataSynced; task identity (title, done state, hierarchy) stays in NgRx and is reached through PluginAPI.updateTask and the ANY_TASK_UPDATE hook. It registers a work-context header button and embeds itself into the work-view embed slot added in the preceding commit. See docs/plans/2026-05-21-document-mode-tiptap-plugin.md for the full design. Squashed from the feat/doc-mode-v4 work; full per-step history is preserved in the archive/doc-mode-v4-full-history branch and the doc-mode-v4-pre-squash tag. |