Commit graph

91 commits

Author SHA1 Message Date
Johannes Millan
c8e5ed2c15 fix(plugins): harden node execution grants 2026-06-09 14:07:06 +02:00
Johannes Millan
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. 2026-06-09 13:56:38 +02:00
felix bear
870d4a4882
docs(backup): document Android automatic restore (#8193)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 11:27:43 +02:00
felix bear
140a0367ea
docs(caldav): clarify bidirectional VTODO sync (#8196)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 11:27:39 +02:00
Filip
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.
2026-06-09 11:25:35 +02:00
felix bear
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>
2026-06-09 00:22:15 +02:00
felix bear
5be516a02c
fix(calendar): disable iCal polling on Android (#8173)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 00:21:17 +02:00
felix bear
a0374e2769
fix(markdown): toggle fullscreen checklist labels #5950 (#8109)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 23:54:52 +02:00
felix bear
e1499af73a
docs(calendar): note per-device credentials (#8185)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 23:51:59 +02:00
felix bear
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>
2026-06-08 20:44:47 +02:00
felix bear
3ef8413b57
fix(config): clear shortcut with delete keys #5604 (#8142)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 20:44:31 +02:00
Johannes Millan
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.
2026-06-08 20:39:02 +02:00
Johannes Millan
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>
2026-06-08 20:21:10 +02:00
Johannes Millan
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 0893a86162. The affectedEntities feature existed solely to make the
atomic completeProject Batch op sync-correct (its only producer). Decoupling
project completion into normal per-task ops (next commit) makes the existing
per-entity conflict detection and effects fire naturally, so this entire
layer — sync-core, super-sync-server, shared-schema, op-log plumbing, the
Prisma migration, and the per-effect completeProject listeners — is no longer
needed. Preserved in history via the checkpoint commit.

* refactor(project): decouple completion from task resolution (Option C)

Completion was an atomic multi-entity Batch op (completeProject) that marked
tasks done / moved them to Inbox inside the project-shared meta-reducer.
Because it bypassed the normal per-task actions, every downstream consumer had
to be taught about it separately — conflict detection (the affectedEntities
feature, reverted in the previous commit), native-reminder cancellation, issue
two-way-sync, time-block and repeat-cfg effects.

Decouple instead: completion is now a plain single-entity PROJECT flag flip
(completeProject = OpType.Update, mirroring archiveProject). Unfinished-task
resolution runs first as the normal per-task actions (moveToOtherProject /
updateTask isDone) from the completion flow, so the existing effects and
per-entity conflict detection fire naturally — no special-casing anywhere.

- project.actions/reducer: restore plain completeProject action + on() handler
- project.service: complete() is a flag dispatch; restore moveTasksToInbox /
  markTasksDone (normal per-task dispatch + Rule #6 flush)
- work-context-menu: resolve unfinished work before the flag flip
- drop the completeProject meta-reducer block, the Batch action, the
  TASK_SHARED_COMPLETE_PROJECT op code, and the reminder-cancel effect
  (unscheduleDoneTask$ already cancels native reminders on the normal path);
  current-task clearing is covered by the existing task-internal effect

Net: ~190 LOC removed here on top of ~1565 (affectedEntities + a Prisma
migration) in the revert. Completion's task resolution is not undone either
way, so the atomic bundle never bought a clean reversal.

* docs(project): record decoupled-completion decision (ADR #5)

Document why project completion uses decoupled per-task resolution + a plain
single-entity flag flip instead of an atomic multi-entity op: the atomic op
forced a cross-stack affectedEntities conflict-detection feature and per-effect
listeners, for an undo guarantee it never delivered. Adds ARCHITECTURE-DECISIONS
#5 and a revision note + corrected undo/bulk-mechanic notes in the plan doc.

* test(project): pin completion ordering + resolution edge cases

Address multi-agent review of the decoupled-completion refactor:
- assert resolution (moveTasksToInbox / markTasksDone) runs BEFORE the
  completeProject flag flip (toHaveBeenCalledBefore) — the core invariant of
  the decoupled design that was previously not pinned
- cover the not-done branch of moveTasksToInbox (no setUnDone) and assert
  markTasksDone dispatches exactly the passed set
- add explicit PCO encode/decode round-trip assertions
- document the inbox-path current-task carry-forward nuance in ADR #5

Composition is covered end-to-end by e2e/tests/project/project-completion.spec.ts.

* refactor(project): tighten completion flow per review

- collapse 3x getCompletionInfo() to <=2: gate the resolve prompt once,
  recompute only after a resolution; each call now wrapped with an error
  snack so a failed archive load no longer aborts silently
- drop the dead post-confirm re-prompt branch (unreachable between two
  sequential single-user modals)
- reuse getDiffInDays + dateStrToUtcDate in completion-stats util instead
  of hand-rolled local-midnight/duration helpers
- use a Set for the top-level-task membership check (was O(n*m))
- drop redundant inline dialog sizing; the panelClass owns fullscreen
- remove dead --project-complete-accent test assertion

* refactor(project): finish review follow-ups for completion flow

- W3: reset the celebration confetti instance on dialog destroy so its
  rAF loop + window resize listener are torn down when the dialog closes
  before the animation ends (ConfettiService now returns the handle and
  fires without awaiting completion)
- S2: extract resolveBgImageToDataUrl() shared by app.component and the
  celebration dialog (was duplicated file://->data-url resolution)
- S3: split completeProject() into _getCompletionInfoOrNotify (dedupes the
  error handling), _promptResolveUnfinishedTasks and _confirmCompletion
- W2: keep prefers-reduced-motion gating app-wide (a11y) + document intent

* fix(project): close confetti teardown race on early dialog close

If the celebration dialog is dismissed while canvas-confetti is still
loading, the instance was assigned after ngOnDestroy ran, so reset() never
fired and the rAF loop + resize listener leaked. Guard with an _isDestroyed
flag and reset the instance immediately if it arrives post-destroy.

Also drop the now-dead CanvasConfetti type alias (superseded by
ConfettiInstance, zero references).

* docs(project): drop non-existent undo-snack from completion wiki

* refactor(project): extract completion task-tree and dialog helpers

* refactor(project): hide Archive menu item; Complete is the retire path

* refactor(project): drop dead archive(), reuse resolve-choice type

Multi-review follow-ups on the completion feature:
- Remove orphaned ProjectService.archive() (+ unused import, spec) — the
  menu collapsed Archive into Complete, leaving no caller. The
  archiveProject action/reducer stay for op-log decode of historical ops.
- Reuse the exported ResolveUnfinishedTasksChoice type instead of
  re-spelling the union three times in work-context-menu.
- Fix misleading moveTasksToInbox comment (setUnDone re-opens, not move).
- Note the as-shipped deviations (no extra selectors, no celebration
  effect) in the design plan so they aren't hunted for later.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-08 13:43:38 +02:00
felix bear
71f4ca484c
fix(plugins): return dialog result #5239 (#8106)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 12:14:08 +02:00
felix bear
00cd665e1a
feat(backup): configure automatic backup file limit #6690 (#8094)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 12:09:49 +02:00
felix bear
376e068412
fix(electron): respect tray title settings #7823 (#8097)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 12:07:09 +02:00
felix bear
667e8c3aa0
fix(azure-devops): simplify host setup #7672 (#8086)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 11:40:19 +02:00
felix bear
c0146e2307
feat(help): add create task how-to #8015 (#8079)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 11:40:14 +02:00
摇摆熊
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>
2026-06-06 17:34:06 +02:00
Maikel Hajiabadi
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
2026-06-06 16:15:49 +02:00
Johannes Millan
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.
2026-06-05 18:10:56 +02:00
Johannes Millan
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
2026-06-05 12:10:14 +02:00
Johannes Millan
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.
2026-06-05 10:53:57 +02:00
Maikel Hajiabadi
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
2026-06-03 14:46:43 +02:00
Johannes Millan
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.
2026-05-26 23:41:01 +02:00
Johannes Millan
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.
2026-05-23 16:36:04 +02:00
Alberto Avon
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 450855b)
- selectAllTasksDueToday: add "should handle missing entity references in planner days with empty task state gracefully" (replacing the taskState.ids variant removed in 9280b85 since upstream guard now owns that invariant)

* feat(tasks): extracted a new selector selectActiveTaskMap that returns a memoized map of active tasks indexed by id

* fix(tasks): removed unused selector

* refactor(projects): move archive/unarchive snacks inside project service and drop the UNARCHIVED_BUT_HIDDEN message

* refactor(projects): use only filtered sources in selectTimelineTasks

* refactor(tasks): rename selectActiveTaskMap into selectMapOfAllTasksInActiveProjects to better align with current naming conventions

* feat(a11y): add aria label to project archive elements

* chore: add comment to selectAllTasksInActiveProjects selector's fast path

* test: add expectation on operation order in archive project action

Navigation must happen after the ProjectService.archive() call

---------

Co-authored-by: Symon <peterbaikov12@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:24:14 +02:00
Florian Bachmann
f3a2621ff3
Allows to use self-hosted Jira instance without web extension (#7628)
* feat(JiraIssueProvider): allow to use self-hosted Jira without web extension

* test(JiraIssueProvider): allow to use self-hosted Jira without web extension

* fix(JiraIssueProvider): fixes problems with fetch error handling

* fix(JiraIssueProvider): fixes problems with fetch error handling

* feat(JiraIssueProv): timeout no longer trips block-access; snack suppressed when block banner shows

* refactor(JiraIssueProvider): minor cleanups on direct-fetch path

- early-return direct-fetch in _sendRequestToExecutor$ so the
  promise / _requestsLog plumbing is only set up for the path
  that actually consumes it
- show global progress bar for direct-fetch requests (countUp /
  finalize countDown) so web users get the same UX as the
  extension path
- match wiki label to the actual i18n string in en.json
  ("Fetch Jira directly from the browser (bypass extension)")

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-05-21 12:45:26 +02:00
Johannes Millan
cdfbf640cb fix(backups): stop automatic backups when disabled 2026-05-20 17:24:34 +02:00
Het Savani
fab5d15cfa
feat(focus-mode): auto-start break after manual session completion (#7681)
* feat(focus-mode): auto-start break after manual session completion

* refactor(focus-mode): clean up manual break flow and update overtime semantics

* refactor(focus-mode): revert unintended Flowtime auto-break behavior

* docs(focus-mode): restore offerFlowtimeBreakOnSessionEnd$ rationale comment

The v3 revert removed the comment block documenting why this effect
listens to endFlowtimeSession rather than pauseFocusSession (which is
fired by sync-stop, idle, and the regular pause button). Restoring it
to match master.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-05-20 15:47:06 +02:00
johannesjo
64c622fab1 feat(tasks): copy focused task title with Ctrl/Cmd+C
Hardens the shortcut: blocks alt/shift modifiers, skips when the
event target is an input/textarea/contenteditable or text is
selected, and guards against the focus-recovery path copying a
stale title. Matches on `ev.code === 'KeyC'` so the shortcut still
fires on non-Latin layouts, mirroring the browser's native copy.
2026-05-19 22:29:41 +02:00
Johannes Millan
74ce49428f fix(sync): support separate Nextcloud login name
Refs #7617
2026-05-18 13:43:23 +02:00
Johannes Millan
eb381c187f fix(plugins): allow iframe-only plugin installs 2026-05-18 13:43:22 +02:00
Johannes Millan
8005b4ec52 feat(supersync): retry transient web fetch failures and surface them as warnings
Adds a 2-retry / 1s+2s backoff loop to the browser/Electron SuperSync
request path, mirroring the existing native retry. A new typed
NetworkUnavailableSPError replaces the previous string-shape contract
between provider and wrapper: the provider throws the typed error from
both web-retry exhaustion and native-failure handler, and the wrapper
matches via instanceof to show a transient WARNING snackbar
(F.SYNC.S.NETWORK_ERROR) without flipping into a hard ERROR state.

Drops the wrapper-side regex classifier (and its defensive
^HTTP\s+\d{3}\b filter), the now-unused isTransientNetworkError alias,
and dedupes the predicate call inside the web-retry catch. The single
remaining call site for the broad regex (operation-log-upload) now
imports isRetryableUploadError directly under its honest name.

Tests cover the 1s/2s cadence, retry-then-success, retry-exhaustion via
the typed error, and negative paths (AbortError, HTTP-status 5xx,
AuthFailSPError must NOT retry). The error class is added to the
cross-module identity safety net.
2026-05-13 11:20:02 +02:00
Johannes Millan
debdedacc0 fix(electron): support today filter in local REST API 2026-05-12 16:33:59 +02:00
Johannes Millan
a59aa39931 feat(tasks): add notes panel shortcut 2026-05-12 12:48:45 +02:00
Johannes Millan
50f79d8e18 Merge branch 'feat/issue-7540-84a0bf'
* feat/issue-7540-84a0bf:
  fix(schedule): indicate tasks beyond available time
2026-05-11 14:34:24 +02:00
Johannes Millan
b15c22db35 fix(schedule): indicate tasks beyond available time 2026-05-11 14:30:55 +02:00
Johannes Millan
cf7f78faa9 fix(focus-mode): improve countdown badge and escape close 2026-05-11 10:09:15 +02:00
agncr
541bc1b0fd
feat(calendar): add per-provider include/exclude regex filters for iCal events (#7528)
* feat(calendar): add per-provider include/exclude regex filters for iCal events

* fix(calendar): mask filterIncludeRegex and filterExcludeRegex in privacy export

Regex filter patterns can contain user-specific title fragments (e.g.
ClientName|Project Alpha) and must be treated as PII.

* fix(calendar): apply regex filters to initial cached calendar emission

Cached events emitted via defer() bypassed regex filters because
provider config was not yet available. The initial emission now reads
selectCalendarProviders first so filterIncludeRegex/filterExcludeRegex
are applied consistently from the start.

* fix(calendar): reconcile banner queue when provider regex filters change

Events queued in _currentlyShownBanners$ were not re-evaluated when a
provider's filterIncludeRegex or filterExcludeRegex changed, so excluded
events could linger until dismissed. The new reconcileBannersOnProviderChange
effect filters the queue against the updated config on every provider change.

* fix(calendar): store raw fetch data in cache before applying regex filters

Regex-excluded events were permanently lost from the cache because
saveToRealLs was called after the filter pass. If a user loosens or
removes a regex while offline, the fallback cache could not restore
those events until the next successful fetch.

The cache now stores unfiltered source data; regex and task/skip/hidden
filters are applied only for the downstream emission.

* fix(calendar): resolve regex filter config per-event in initial cached emission

The defer() path derived one provider config from entry.items[0]?.calProviderId
and applied it to every item in the cache entry. Mixed-provider entries
would apply provider A's regex to provider B's events.

Config is now resolved inside the filter callback via calEv.calProviderId,
matching how _groupCachedEventsByProvider() already handles per-item grouping.

Regression test added with a mixed-provider cached entry verifying that
each event is filtered against its own provider's config.

* fix(calendar): apply full view-level filters to initial cached emission

The defer() path only applied regex filters, so task-imported, skipped,
and hidden events could briefly reappear on startup until the first fetch
completed. Now reads all four filter sources in a single combineLatest
snapshot and applies the same ID-based checks as the live fetch path.

Regression test added for all three ID-based filter types.

* fix(calendar): reapply regex filters from cache

* fix(calendar): hide stale cached provider events

* fix(calendar): filter manual iCal search

* fix(calendar): refresh agenda filters

---------

Co-authored-by: agncr <a.gencer@thalia.de>
Co-authored-by: agncr <agncr@users.noreply.github.com>
Co-authored-by: johannesjo <johannes.millan@gmail.com>
2026-05-11 00:12:01 +02:00
Het Savani
be4107bb37
feat(task-repeat): add repeat after completion option (#7524)
* feat(task-repeat): add repeat after completion option

* fix(task-repeat): address review feedback for wait-for-completion

* fix(task-repeat): align wait-for-completion recurrence behavior

* fix(task-repeat): restore completion-based recurrence anchoring

* fix(task-repeat): stabilize latest-instance checks and clean up docs

* fix(task-repeat): avoid errors when no repeat occurrence is due

* fix(task-repeat): align first occurrence identity

* fix(task-repeat): handle legacy and archived completion

* fix(task-repeat): probe wait gate after any completion

* docs(task-repeat): clarify wait completion blockers

---------

Co-authored-by: johannesjo <johannes.millan@gmail.com>
2026-05-10 23:28:22 +02:00
Johannes Millan
882203ecb9 feat(theme): add background image blur slider 2026-05-09 18:37:11 +02:00
Johannes Millan
773dd033b7 fix(theme): clarify background tint setting visibility 2026-05-09 18:02:16 +02:00
David Vornholt
913426155a
feat(schedule): add week view scaling (#7530)
* fix(ui): improve task selector and focus notes controls

* feat(schedule): add week view scaling
2026-05-09 15:11:46 +02:00
cpa-27
1d1b49d337
fix(docs): Add documentation for time estimate short syntax (#7454)
* fix(docs): Update new task placeholder to include time estimate short syntax example

* fix(docs): Update wiki entry to reference time estimate short syntax tags
- Updates new task placeholder to reflect changes made in b09aa6e
- Adds list item explaining time estimate short syntax
- Adds reference to wiki short syntax page
2026-05-02 13:57:55 +02:00
Nicholas Dias
7a51d4b06e
feat: allow dev override for local REST API (#7440) 2026-05-01 16:54:26 +02:00
Florian Bachmann
6c1a0b54f9
Caldav subtask import (#7409)
* fix(electron): disable webSecurity in dev mode to suppress CORS preflights

* feat(CalDAV): Option to import sub-tasks along with parent

* test(CalDAV): Adds specs for option to import subtasks along with parent

* doc(CalDAV): Adds notion on automatic sub-task import

* feat(CalDAV): Handles grandchildren and ignores subtasks of archived tasks.

* fix(setup): removes policy violating code

* fix(caldav):_resolve N+1 issue

* test(caldav):_adds more tests

* fix(electron):_fix task-widget mock in electron test
2026-05-01 13:07:03 +02:00
Het Savani
baaafbab44
feat:Add a keyboard shortcut to collapse/expand groups (#7370)
* feat:Add a keyboard shortcut to collapse/expand groups

* fix(collapsible): scope group behavior to isGroup and address review feedback

* fix(collapsible): scope group behavior, fix a11y focus, clean CSS, and update docs

* feat(work-view): apply isGroup to Overdue/Later/Done/Recurring sections

Aligns implementation with the wiki keyboard-shortcuts docs added in this
PR — those sections are described as participating in the expand/collapse-all
group shortcuts but were missing the [isGroup]="true" input.

* feat(tasks): ArrowLeft on top-level task collapses parent group

When a top-level task (no parent) is focused and the existing arrow
handling would otherwise call focusPrevious, hand off to the closest
ancestor collapsible group instead: collapse it and move focus to the
group header. Makes the new group shortcuts reachable from the normal
task-driven keyboard flow rather than requiring a Tab to the header.

Sub-tasks and selected-task hide-detail behavior are unchanged.

* feat(collapsible): ArrowUp/Down on group header focuses adjacent task

When a group header is focused, ArrowDown moves focus into the next
<task> element in document order (the first task of the group when
expanded; the next group's first task when collapsed). ArrowUp moves
focus to the previous task. Pairs with the ArrowLeft hand-off so the
group shortcut and task navigation form a single continuous keyboard
flow.

* feat(tasks): ArrowUp/Down on group-edge task focuses the header

ArrowUp on the first task of a group focuses the current group's
header; ArrowDown on the last task focuses the next group's header.
Routed through new handleArrowUp/handleArrowDown methods so other
focusPrevious/focusNext callsites (move-to-backlog, etc.) keep their
existing semantics.

* refactor(util): extract findAdjacentFocusable for arrow nav

Three places had near-identical DOM-walking logic for "find the next/prev
focusable element across tasks and group headers". Consolidates them
into a single utility with 9 unit tests, plus a shared GROUP_NAV_SELECTOR
constant exported from collapsible.component.

Behavior change: ArrowUp/Down on a group header now considers other
collapsed group headers as next-focusable (previously walked only across
<task> elements). For an all-collapsed list, ArrowDown on a header now
moves to the next header instead of skipping to a far-away task.

* refactor: trim findAdjacentFocusable + tighten collapsible API surface

Multi-agent review consensus:
- Drop the compareDocumentPosition fallback in findAdjacentFocusable.
  Both real callers pass an element matching the selector; the fallback
  branch only existed to satisfy a synthetic test. Util shrinks to ~10
  lines of obvious code.
- Re-privatize setAllGroupExpanded and expandIfCollapsed — neither is
  called from outside CollapsibleComponent.
- Scope focusHeader's selector to ':scope > .collapsible-header' so a
  nested collapsible projected through ng-content can't steal focus.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-04-27 14:38:44 +02:00
Spyros Seimenis
fede2c6ada
feat(gitea): add label-based include and exclude filters (#7359)
* feat(gitea): add label-based include and exclude filters

Adds two optional config fields to the Gitea issue provider. filterLabels
is a comma-separated allowlist forwarded to the issues endpoint as the
`labels` query param (AND-matched server-side). excludeLabels is a
comma-separated denylist applied client-side, since Gitea/Forgejo's
issues endpoint has no negation syntax. Scoped labels (scope/name) work
with both.

This lets users point multiple Gitea integrations at the same repository
and route disjoint subsets of issues into different Super Productivity
projects, e.g. one project filtering `project/foo` and another excluding
it.

Filters apply only to ingestion (search suggestions and auto-import of
open issues). Already-imported tasks keep refreshing regardless of label
changes, matching how the existing `scope` filter behaves.

* docs(gitea): document label include/exclude filtering

Update the issue-integration comparison page with the new label
allowlist and denylist filters, including scoped-label support.

* feat(gitea): enforce filterLabels AND match client-side

Gitea's two issue endpoints (`/repos/{o}/{r}/issues` and
`/repos/issues/search`) parse the `labels=` query param into different
internal fields and produce different semantics (AND vs OR), so the
same `filterLabels` config behaved differently on auto-import vs
manual search. The AND on the auto-import path is also a Gitea bug
(go-gitea/gitea#33509) that may flip to OR upstream at any time.

Apply AND-matching on the client (mirroring how `excludeLabels`
already filters client-side) so behavior is consistent across both
endpoints and independent of server version. The `labels=` query is
still sent as a coarse pre-filter (always a superset of the right
answer regardless of server semantics). Forgejo inherits the same
endpoints from Gitea, so the same fix covers it.
2026-04-26 19:10:18 +02:00