* feat(sync): show actionable error on persistent WebDAV 409
When a WebDAV PUT keeps returning 409 Conflict even after the parent
collection is created, the Base URL / Sync Folder Path is misconfigured
(the classic Synology / raw-WebDAV setup mistake). Previously the raw
"HTTP 409 Conflict" (or a bare "Unknown error") reached the user with no
hint at the cause.
Throw a dedicated WebDavSyncFolderUnusableSPError with a privacy-safe,
actionable message (no path, no response body) at the spot that already
detects this condition. Mirrors NetworkUnavailableSPError: a fixed
user-facing message matched by instanceof.
* ci(release): revive contributors section in release notes
* fix(plugins): stop leaked timers on plugin disable via onUnload hook
* fix(plugins): harden onUnload teardown hook after review
* refactor(plugins): group lifecycle registers into options object
* fix(plugins): close onReady stale-guard gap and timer interleave race
* fix(caldav): discover calendars via principal/calendar-home-set #8259
The CalDAV Events plugin populated its calendar dropdowns with a single
Depth:1 PROPFIND on the exact URL the user entered, keeping only
responses whose resourcetype is <calendar>. Users paste the advertised
CalDAV root (Nextcloud /remote.php/dav, Fastmail /dav/), where calendars
live one or two collection levels deeper, so the request succeeds but
lists no calendars. The dialog then showed a green 'Options loaded' tick
with empty dropdowns (loadOptions resolved with an empty array, no
error), and the Schedule showed no events because no calendar could be
selected.
Rewrite discoverCalendars to do RFC 4791 service discovery: try the
entered URL directly (back-compat for a pasted calendar-home), else
PROPFIND current-user-principal, then calendar-home-set, then enumerate
the home collection.
Discovery now follows server-controlled hrefs, so harden resolveHref to
resolve via the URL constructor and refuse any off-origin target (the
SSRF boundary, since credentials attach to every request), and drop
untrusted server data from thrown error messages. De-duplicate the XML
parse-error guard into a shared parseXmlDoc.
Add discovery test coverage (previously none), including cross-origin
href refusal.
* test(caldav): assert request origin instead of URL substring
CodeQL flagged the cross-origin discovery test's url.includes('evil...')
check (js/incomplete-url-substring-sanitization). Parse each requested
URL and assert its origin equals the entered server origin instead — a
stronger, alert-free assertion that no credentialed request escaped
off-origin.
* fix(plugins): harden node execution grants
* fix(plugins): harden iframe bridge boundaries (#8208)
* fix(plugins): harden iframe bridge boundaries
* fix(plugins): tighten iframe bridge follow-up
* test(plugins): make node-executor electron test hermetic
The new plugin-node-executor test read the real built-in plugin manifest
(src/assets/bundled-plugins/sync-md/manifest.json), which is a build
artifact absent when 'npm run test:electron' runs in CI (before the
frontend/plugin build). Stub the manifest read, scoped to the executor
module via Module._load, so the grant/token/webContents assertions no
longer depend on built plugin assets.
* fix(plugins): allow iframe formatDate & getCurrentLanguage i18n methods
The iframe API allow-list gate added in #8208 only listed a subset of
the i18n methods that master's #8146 exposes to iframe plugins. Without
this, plugin calls to formatDate/getCurrentLanguage (and translate) are
rejected with 'Unknown API method'. Add all three so the merged gate
matches the methods createBoundMethods/createPluginApiScript expose.
* docs(plugins): document node-exec handoff bootstrap-ordering invariant
The one-shot consumePluginNodeExecutionApi() handoff is defended by
construction ordering, not structural isolation: PluginBridgeService must
consume it before any plugin 'new Function' code runs (both share window.ea
in one renderer realm). Document the invariant at the consumption site so a
future lazy-service/early-plugin-load refactor can't silently regress it.
Surfaced by the post-merge security review (latent finding; not currently
exploitable).
* fix(plugins): use static iframe sandbox attribute to avoid NG0910
Binding a security-sensitive iframe attribute (sandbox) via [attr.sandbox]
makes Angular throw RuntimeError NG0910 and tear down the iframe, crashing
the plugin view to the global error screen. This broke the plugin-iframe,
plugin-loading and plugin-lifecycle e2e tests.
Restore the static sandbox attribute (still without allow-same-origin, so
the opaque-origin isolation from #8208 is preserved) and drop the now-unused
iframeSandbox binding/import.
* feat(plugins): migrate Gitea and Linear issue providers to plugins
* feat(plugins): localize Gitea issue provider config form
Port the existing Gitea translations (F.GITEA.* + shared issue-content keys)
into the plugin's own i18n so non-English users keep localized config labels
after the plugin migration. Generated for all 28 app locales.
* docs(plugins): note Linear teamId/projectId filter is now active
The built-in Linear provider defined teamId/projectId config fields but never
passed them to the search, so they were inert. The plugin honors them as the
field labels promise; document this as a deliberate behavior change.
* test(issue): fix unknown casts in issue-provider migration spec
The spec compiled clean under the app tsconfig (which excludes *.spec.ts)
but failed CI's tsconfig.spec.json build: casting state.entities[id]
(IssueProvider | undefined) straight to Record<string, unknown> is a
TS2352 non-overlap error, and .toBe(already) on a bare literal is a
TS2345. Add the intermediate 'as unknown' so the spec type-checks.
* ci(lighthouse): raise total resource-count budget to 260
Migrating the Gitea and Linear issue providers to bundled plugins adds
two more entries to BUNDLED_PLUGIN_PATHS. Each bundled plugin is
discovered at startup, fetching its manifest.json plus icon.svg, which
pushed the Lighthouse total request count to 251, one over the previous
250 budget. The increase is an inherent, accepted consequence of the
built-in -> bundled-plugin migration (same as github/clickup); bump the
budget to 260 to reflect the new baseline with modest headroom.
* 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.
With Auto time blocking on, scheduling a task writes a mirror event to
Google Calendar. When the read calendar overlaps the time-block calendar,
that mirror was fetched back and rendered next to the local task that
created it — a duplicate. Filter SP-owned events (tagged with
extendedProperties.private.spTaskId) out of all plugin read paths.
* 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.
* fix(caldav-plugin): make recurring-occurrence edits/deletes safe and quiet #7492
Expanded RRULE occurrences all share one master .ics, so operating on a
single occurrence hit the whole series: updateIssue rewrote the master
(and the next poll pulled it onto every sibling), deleteIssue deleted the
master (the entire series), and getById returned the master's DTSTART,
collapsing every instance onto the first one's time.
Plugin:
- parseCompoundId surfaces occurrenceMs instead of discarding it
- getById re-anchors start/end onto the requested occurrence (timed via
toIcalUtcDateTime, all-day via toIcalDate matching ical.js local-midnight)
- updateIssue/deleteIssue refuse occurrence writes with a marked error
(isExpectedSyncSkip), so the series is never mutated
Host two-way sync:
- editing or deleting a task linked to one occurrence now stays silent (the
user changed their task, not the calendar) and the local change still
applies. Explicit agenda reschedule/delete keep surfacing the honest
"can't change a single occurrence" message (no false success).
Single non-recurring events are unaffected. Full per-occurrence editing
(RECURRENCE-ID overrides + EXDATE) remains a follow-up; it needs an If-Match
primitive and recurrence-value preservation.
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.
* chore(deps): bump plugin-dev dev tooling to patch security alerts
Update vite, vitest, postcss, glob, and minimatch across the
packages/plugin-dev/* sample plugins to clear 26 Dependabot alerts.
These are dev/build tooling for the example plugins and are not
shipped to end users.
- vite >=7.3.2, vitest 4.1.x, postcss 8.5.15, glob 10.5.0,
minimatch 9.0.9/5.1.9 (all within declared semver ranges)
- clickup-issue-provider: vitest ^3.2.1 -> ^4.1.0 (only manifest change)
Verified test suites pass: clickup (28), automations (109).
* chore(deps): bump minimatch + webpack-dev-server overrides for security
Resolve the remaining root-lockfile Dependabot alerts in build/dev
tooling (not shipped to end users):
- minimatch: the existing `app-builder-lib` override pinned minimatch to
10.1.1, which npm cascades across electron-builder's entire subtree
(@electron/asar, @electron/universal, dir-compare, filelist, temp).
Bump the override to 10.2.5 (within app-builder-lib's ^10.0.3); the
vulnerable nested copies dedupe to the patched top-level. Clears
Dependabot #341, #372, #373 (minimatch ReDoS).
- webpack-dev-server: add override to 5.2.4. Clears #593.
Note: electron-builder `npm run dist` packaging not run in this env;
the minimatch bump is a patch within range so impact is expected nil.
* perf(plugins): skip no-op doc-mode saves via lastSeenDocBytes equality
Closes#7815.
flushSave and flushSaveSync now compute the would-be-written raw bytes
first and short-circuit when they equal lastSeenDocBytes — a typed-then-
reverted cycle inside the 30s throttle window no longer produces a
postMessage round-trip, an IDB transaction, or an op-log entry.
The baseline is intentionally NOT updated on a skip: it already equals
these bytes, so the self-echo invariant for PERSISTED_DATA_CHANGED
(#7752) is preserved.
Drive-by: added the isDocCorrupt guard to flushSave for symmetry with
flushSaveSync / scheduleSave. The schedule gate normally prevents
flushSave from firing on a corrupt doc, but corruption can be set
between schedule and fire (e.g. a remote-update reload turning up an
unparseable blob), so the explicit guard is cheaper than reasoning
about that invariant.
Persistence rename: saveContextDoc → persistContextDocRaw. The caller
now owns serialisation (via serializeContextDoc) so the byte-compare
can run before the persist dispatch. Three persistence.spec.ts tests
replace the dropped saveContextDoc test: exact-raw write, encoder
determinism (the premise the byte-compare relies on), and
write-idempotence (which proves the skip loses no information).
* refactor(plugins): apply doc-mode no-op-save review findings (#7815)
Multi-review follow-ups for #7815:
- Tighten serializeContextDoc determinism test: previously asserted
equality between a doc and its `{...doc}` spread, which preserves V8
insertion order — the test would pass even against a future encoder
that randomised iteration over Map-backed nodes (the real failure
mode for the byte-compare). Now asserts repeated-call determinism on
the same input directly, plus type+non-empty shape so a future return-
type change (e.g. Uint8Array) breaks here, not in the editor.
- Document the sync/async stamp asymmetry in flushSaveSync: it stamps
lastSeenDocBytes BEFORE the void dispatch, whereas flushSave stamps
AFTER `await`. Both are correct (the sync path is fire-and-forget and
the iframe can be torn down mid-call, so a post-dispatch stamp would
silently drop on teardown) but the divergence wasn't called out in
the inline comments.
* refactor(plugins): rename document-mode to doc-mode
Renames the bundled plugin's id (`document-mode` → `doc-mode`), display
name (`Document Mode (Alpha)` → `Doc Mode (Alpha)`), package name,
directory, and asset path. No migration: the plugin has never been
published, so there is no on-disk user data to preserve.
Touched everywhere the id was hardcoded:
- Plugin manifest, package.json, build/deploy/test scripts, log prefixes
- Host BUNDLED_PLUGIN_PATHS entry (src/app/plugins/plugin.service.ts)
- Host comments and test fixtures (plugin-hooks, plugin-persistence-key,
plugin-persistence.model, conflict-resolution.service.spec)
- E2E spec filenames + PLUGIN_ID constant + label assertions
- build-all.js plugin orchestrator
- build/release-notes.md user-facing entry
Test pass: 88/88 plugin specs, 8/8 plugin-hooks, 132/132
conflict-resolution, 15/15 plugin-persistence-key.util.
Bundle redeployed to src/assets/bundled-plugins/doc-mode (gitignored).
The old `src/assets/bundled-plugins/document-mode/` dir is gitignored and
left on disk after this commit; the host loader no longer references it,
so it's inert. Remove with `rm -rf src/assets/bundled-plugins/document-mode`.
* refactor(plugins): apply doc-mode rename review findings
Multi-review follow-ups for the rename in 3443bcacd0:
- editor.ts:1920: missed runtime log string ('Document mode:' →
'doc-mode:') — leaked into the dev console with inconsistent brand
because the rename sweep matched on the kebab `document-mode` or the
PascalCase `Document Mode`, not the bare-space `Document mode`.
- editor.ts + background.ts header JSDoc: `Document-Mode editor` /
`Document-Mode background script` → `Doc-Mode …`. Cosmetic but the
only remaining branded references in the source.
- scripts/build.js: esbuild iife `globalName: 'DocumentModePlugin'` →
`'DocModePlugin'`. Internal global, no consumers, but worth keeping
consistent with the new id.
- src/app/plugins/plugin.service.ts: add a header comment to
BUNDLED_PLUGIN_PATHS noting that pluginIds become entityId prefixes in
IDB / op-log / sync wire and must be treated as permanent for any
plugin that has ever shipped. Records the rule we relied on being
absent for this rename.
DOM-document references (`installDocumentDragHandlers`,
`Document-status banner`, `Document-level pointer handlers`) left
unchanged — those refer to the browser document object, not the brand.
Historical plan docs in `docs/plans/2026-05-2[123]-*.md` also left
unchanged per the rename commit's intent.
* 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.
* fix(plugins/github): correct issue-number prefix on imported tasks
The title field mapping mishandled issue imports two ways:
- Search/backlog results carry no `number`, so ctx.issueNumber was
undefined and titles got a literal "#undefined " prefix. Fall back
to ctx.issueId (equals String(issue.number) for GitHub).
- mapSearchResult already display-prefixes the title with "#<n> ", so
re-applying it yielded "#<n> #<n> ...". Short syntax then treated the
non-leading "#<n>" as a tag and stripped it. toTaskValue is now
idempotent so the prefix lands exactly once.
* fix(tasks): ignore numeric #tags in issue task titles
An issue task's leading "#<number>" is the issue id, and issue titles
routinely reference other issues ("fixes #1234"). Short syntax parsed
those non-leading "#<n>" tokens as tags, prompted to create them, and
stripped them from the title. Extend the existing numeric-at-start
guard to also block numeric "#<n>" anywhere on issue tasks. Non-numeric
tags ("#mytag") still parse, so manual tagging of issue tasks via the
title keeps working.
* 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
* chore(plugins): re-bundle document-mode and document Stage A path
Reverts the unbundling from b0cae69ffe. Stage 0 (gzip + throttle, shipped
in 84625be849) handles size; document-mode remains opt-in per context, so
cross-context conflict risk is bounded. Stage A (keyed plugin-persistence
API, issue #7749) is the documented future path for closing the LWW gap
on different-context concurrent edits — picked up when conflicts are
observed in practice.
Design sketch with multi-reviewed phasing lives in
docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md. Predecessor
plan's "Future work" section now links to it.
* test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
Follow-ups from the multi-review of 199e816479's re-bundling decision:
- E2E smoke test asserts document-mode appears in plugin management so
a typo in BUNDLED_PLUGIN_PATHS fails loudly.
- Spec exercises PLUGIN_USER_DATA conflict resolution end-to-end, which
previously relied on analogy to REMINDER (same array+null branch) but
was never directly asserted after the migration off 'virtual'.
- Stage A plan risks: stale-editor-view gap surfaced by the review;
PluginHooks.PERSISTED_DATA_UPDATE already exists in the API but is
never dispatched host-side — wiring it is the path to a fix.
- background.ts: comment marks the known gap at the registerHook site.
* docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
Designs the host-side wiring for the currently-dead
PluginHooks.PERSISTED_DATA_CHANGED so plugins can react to remote-driven
changes to their persisted data. Multi-reviewed twice; v4 trims scope to
host-only (no plugin adoption in this design) and preserves the
multi-review insights as seeds for the follow-up doc-mode adoption
tracked at issue #7752.
Implementation lands in a separate PR.
* test: strengthen unit test assertions and revive disabled plugin specs
Replace tautological assertions, setTimeout-without-expect patterns, and
placeholder `expect(true).toBe(true)` tests with real assertions across
~30 spec files. Revive 5 plugin spec files that were fully commented out
on master with live tests covering core behavior.
Production-side: extract pure helpers for testability:
- app.component: getBackgroundOverlayOpacity, getBackgroundImageBlur
- android-sync-bridge.effects: getSuperSyncCredentialBridgeCommand
- super-sync-server: export escapeHtml, SERVER_HELMET_CONFIG
Delete the always-skipped xdescribe placeholder
src/app/imex/sync/sync-fixes.spec.ts (412 LOC).
Rename operation-log-stress.spec.ts to .benchmark.ts to match its
header comment ("excluded from regular test runs").
No production behavior changes; no master commits reverted.
The document-mode plugin persisted each taskRef/subTaskRef chip with its task title as inline content plus an isDone attr. Both are re-derived from the host task cache on load (migrateStoredDoc backfills content, refreshChipContentFromCache overwrites both), so storing them only inflates the synced blob -- and the title is its byte-heavy part.
Add a pure stripChipContent() that collapses every chip to a bare identity atom { type, attrs: { taskId } }, and apply it in flushSave / flushSaveSync before serializing. The live editor document keeps its inline content, so title write-back is unaffected; only the persisted copy shrinks. A bare-atom chip loads through the existing unchanged pipeline, so no schema bump or migration is needed. Roughly halves the per-change sync payload.
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.
The currentTaskChange handler destructured { current, previous } from
the hook payload with no nullish guard, unlike its sibling handlers
(taskCreated/taskComplete/taskUpdate) which all check payload first.
A nullish payload threw TypeError into PluginHooksService.dispatchHook,
logged as a plugin hook handler error. Guard the payload before
destructuring, matching the existing handler pattern.
Google Calendar time-block sync failed with 403 rateLimitExceeded. Root cause: every upsert did POST insert -> 409 duplicate -> PATCH (two writes plus a guaranteed 409 per sync), and a single user edit dispatched several actions that each issued a write to the same event within ~1s, bursting past Google's per-event write limit.
- google-calendar-provider: patch-first idempotent upsert (POST only on 404; recover a concurrent-create 409 via PATCH) + bounded exponential backoff on 403 rateLimitExceeded / 429, retried per network call. Follows Google's documented 409 remedy.
- caldav-calendar-provider: PUT is already an idempotent upsert; add the same bounded backoff on transient 429/503 from self-hosted servers. 404-swallow on delete unchanged.
- time-block-sync.effects: merge createOrUpdateOnSchedule$ + updateOnFieldChange$ into one upsertOnTaskChange$ that groups by task id and debounces (COALESCE_MS=1000) then switchMaps, re-reading the latest task post-debounce. One settled edit = one write. Still LOCAL_ACTIONS + action-driven (sync-correctness rules 1-2 preserved).
Adds vitest coverage for both plugins' timeBlock paths and the first effects spec for time-block sync.
Adds two new triggers and a new action to the bundled automations plugin,
along with the host-side and validator changes needed to make them
reliable end to end.
Plugin
- New triggers TriggerTaskStarted / TriggerTaskStopped that fire on timer
start / stop. Switching from task A to task B emits taskStopped(A) and
then taskStarted(B), so a rule subscribed to either trigger gets the
full lifecycle. The trigger descriptions document this explicitly.
- New ActionRemoveTag mirroring ActionAddTag, with a shared resolveTagId
helper covering id/title lookup, missing-tag warning, and idempotent
short-circuiting.
- Import goes through a new RuleRegistry.addRules(rules[]) + addRules
message, so the host's 1 call/sec persistDataSynced rate limit no
longer rejects multi-rule imports.
- Validators in core/rule-registry.ts and utils/rule-validator.ts share a
set of `as const` arrays guarded by a TS _AssertEq exhaustiveness check
against the union types in types.ts. The arrays are duplicated rather
than exported from types.ts because that would turn types.ts into a
shared runtime chunk and break plugin.js (which the host evaluates via
`new Function`, not as an ES module).
- Import validator no longer requires a truthy rule name — the UI saves
empty-named rules, the load-path accepts them, and the import path
needed to match.
- ActionDialog gets a removeTag placeholder. Manifest's hooks list is
updated to include the hooks the plugin actually registers
(taskCreated, currentTaskChange) so the permission disclosure UI is
accurate.
Host
- plugin-hooks.effects.ts onCurrentTaskChange$ now observes
selectCurrentTask directly instead of only setCurrentTask /
unsetCurrentTask actions. This catches transitions through reducer
paths that don't dispatch those actions (loadAllData, deleteProject,
bulk task delete via the shared CRUD meta-reducer).
- The payload changes from the raw new Task to { current, previous },
matching the long-declared CurrentTaskChangePayload shape. The effect
uses pairwise on the selector and filters same-id pairs at the event
boundary (not via distinctUntilChanged on the source) so the
`previous` task always carries the latest snapshot — including
in-place updates a plugin made while the task was running. Without
this, addTag-on-start / removeTag-on-stop only worked every other
cycle because `previous` reflected the pre-mutation snapshot.
Other plugins consuming currentTaskChange (voice-reminder, api-test-plugin)
read payload.current instead of the raw Task.
The new theme upload button's matTooltip ("...never run code...") leaves its
text in the DOM via cdk-describedby, so case-insensitive getByText('never')
matched both the "Never" reminder option and the tooltip. Target the
mat-option by ARIA role and exact case, and scope the post-schedule
assertion to the dialog so only the mat-select trigger text qualifies.
Also prunes a stale nested vitest/esbuild tree from the caldav plugin's
package-lock.json.
The web build at app.super-productivity.com was inheriting the desktop
OAuth client whose accepted redirect URIs are loopback-only, so Google
rejected the https callback with redirect_uri_mismatch.
Add webClientId / webClientSecret to OAuthFlowConfig and route the
browser branch through them. Google's "Web application" client type is
provisioned as confidential, so PKCE alone is not enough — the secret
ships in client JS like the desktop one. Electron and native mobile
flows are unchanged.
Bundle ical.js into the plugin and use it for the read path. The hand-rolled
parser stays for getById/updateIssue/deleteIssue. Anchor the sync window to
start-of-UTC-day so in-progress events stay visible, count only emitted
occurrences toward the per-event safety cap, and isolate per-VEVENT failures
so one malformed event can't drop the rest. Compound id uses '#occ=<ms>' so
a server-controlled href cannot collide with the occurrence delimiter.
Make explicit in the backlog-query help text that the default behavior
without a token imports every open issue, not just the user's, so anyone
enabling auto-import without credentials understands the scope.
The default backlog query 'sort:updated state:open assignee:@me' fails with
HTTP 422 ("The listed users cannot be searched...") when the provider has
no token configured, because GitHub's Search API can't resolve @me without
an authenticated request. This broke auto-import for public repos, which
the docs and UI advertise as not requiring a token.
Fall back to 'sort:updated state:open' when no token is set, and update
the help text to describe both defaults.
Closes#7381
Adds an advanced checkbox on the GitHub issue provider that, when enabled,
drops the hardcoded `is:issue` filter in both backlog auto-import and
in-provider search. Lets users sync PRs (assigned, authored, or
review-requested) into their backlog with a query like
`state:open is:pull-request (assignee:@me OR review-requested:@me)`.
Default is off, existing behavior is unchanged.
packages/vite-plugin/package.json was bumped to vite ^6.4.2 in
4b9bd23e56 but dependent plugin lockfiles still referenced ^6.0.0.
npm install during the e2e build brought them in sync and hoisted
vitest's nested esbuild binaries, dropping ~500 redundant entries.
encodeURIComponent leaves ( and ) intact per RFC 3986, but GitHub's
search API treats them as grouping operators that must be percent-encoded,
returning HTTP 422 on queries like "(author:@me OR assignee:@me)".
The in-repo provider was fixed in 960330dd56 but the fix didn't carry
over when GitHub moved to a plugin. Reapply via a small encodeGithubQuery
helper used at both call sites.
* fix(issue): prevent crash from orphan issueProviderId (#7135)
The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.
Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.
* fix(focus-mode): sync tray countdown with in-app timer during breaks
Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.
Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.
Fixes#7278
* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68
Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.
Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3
* fix: restore i18n, UI, and docs work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.
Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior
* fix(electron): restore electron + e2e work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)
Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
(re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
(setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
(electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
(SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
(this is literally the fix the bad commit's message claimed to add)
Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
checks from d64014d086 (later than c558bcab5e) while restoring the
goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
clearStaleLevelDbLocks from theirs, fs from ours).
* fix(sync): restore sync-core work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.
This PR restores only the pieces that are genuinely still missing:
- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
stale-idb-locks.ts piece was restored in the prior commit)
Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)
Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
* Implement task parsing with sub-tasks
Added a new function to parse tasks with sub-tasks from a text input, improving task management capabilities. Updated the task addition logic to utilize this new parsing method.
Should solve #7183
* Enhance task parsing and user information
Key Changes Summary:
✅ Critical Fix#1 - 4-space indentation: Detects minimum indent and normalizes relative to it
✅ Critical Fix#2 - Mixed input: Plain-text lines no longer silently dropped; warning shown
✅ Critical Fix#3 - Deeply-nested items: Now flattened to sub-task level with user notification
✅ Minor - Sub-task dueDay: Documented that it inherits from parent (not passed independently)
✅ Suggestion - UI/UX: Updated placeholder text to show expected format with examples
* fix indentation calculation and missing isBullet: true
* fix(brain-dump-plugin): keep sub-tasks when plain-text lines interrupt
Previously, a plain-text line between a bullet and its indented sub-task
caused the sub-task to be silently dropped: the outer loop advanced past
the plain-text entry but the inner look-ahead had already exited at
indent 0, so the following indented bullet was never attached to the
parent and was rejected by the outer loop's else branch.
The sub-task scan now skips over plain-text lines while still breaking
at the next top-level bullet, so indented bullets attach to the previous
parent regardless of interleaved plain text. The plain-text line itself
is still emitted as its own top-level task by the outer loop.
Also drops dead code:
- findMinimumIndent() was declared but never called.
- dueDay on sub-task payload was forwarded but silently discarded by the
plugin bridge (sub-tasks inherit the parent's date).
And de-duplicates the "Note: ... Note: ..." prefix in the deeply-nested
warning snack.
---------
Co-authored-by: Adnoh <git@rotzefull.de>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
* test(e2e): update Show/hide task panel button label
The TOGGLE_DETAIL_PANEL translation was renamed from "Show/Hide
additional info" to "Show/hide task panel" in dd789d2, but e2e
selectors still referenced the old string, causing every test that
opens the task detail panel to time out.
https://claude.ai/code/session_011mX4Pr24S42svmA5j7fRux
* 18.2.1
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)