Commit graph

60 commits

Author SHA1 Message Date
Lane Sawyer
87f15c8c65
chore(config): adopt AGENTS.md as shared AI-agent config with skills (#8864)
Make AGENTS.md the single source of guidance read natively by Claude
Code, OpenAI Codex, and GitHub Copilot. CLAUDE.md becomes a symlink to
it. Extract commit-message guidance into a reusable Agent Skill under
.agents/skills (read by Codex and Copilot); .claude/skills symlinks to
it for Claude Code, and .gitignore is narrowed to /.claude/* so only
that skills pointer is tracked. Drop the now-redundant
.github/copilot-instructions.md symlink since Copilot reads AGENTS.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:30:56 +02:00
Johannes Millan
a740618d50 docs: prefer GitHub Actions for E2E suites 2026-07-02 15:15:52 +02:00
Johannes Millan
932e2f8ec9 docs: add product principles / anti-feature-creep guidance to CLAUDE.md 2026-06-26 13:12:02 +02:00
Johannes Millan
bf2f8840cb docs(claude): flag long-term costs/risks in feature code reviews 2026-06-18 15:11:11 +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
Johannes Millan
e9ceb841d6 docs: flag task component (perf) and sync system (correctness) as high-risk areas 2026-06-08 18:29:55 +02:00
Johannes Millan
694f257631 docs(contributing): flag one-off Material style overrides 2026-06-05 14:31:56 +02:00
Johannes Millan
42e6626b76 docs(sync): consolidate sync docs + enforce the contributor model
Collapse the sprawling, partly-stale docs/sync-and-op-log/ tree into a
small authoritative set and make the sync-correctness invariant
partly lint-enforced instead of convention-only.

Docs:
- Delete superseded/duplicate/provably-stale design, plan, and
  background-research docs (quick-reference, the architecture-diagrams
  monolith, the "Hybrid Manifest" docs describing code that does not
  exist, completed long-term plans, LLM-synthesis analyses).
- Salvage load-bearing decision history into the surviving docs before
  deletion: rejected-alternatives rationale -> operation-log-architecture
  ("Why this architecture"); vector-clock pruning incident history ->
  vector-clocks.md; archive-payload optimization -> architecture E.7.
- Add contributor-sync-model.md as the single-invariant entry point
  (one user intent = one op; replayed/remote ops must not re-trigger
  effects), with a decision table mapping to the enforcing linters.
- Repoint external/internal cross-refs; add CONTRIBUTING.md + CLAUDE.md
  pointers; record the migration in a dated docs/plans/ design doc.

Enforcement (new eslint-local-rules):
- no-actions-in-effects (error): effects must inject LOCAL_ACTIONS /
  ALL_ACTIONS, never the raw @ngrx/effects Actions stream.
- no-multi-entity-effect (warn, heuristic): flags a literal returned
  array of >=2 action-creator calls; docstring + valid-case specs pin
  exactly which shapes are and are not detected.
- run-specs.js runner wired into `npm run lint` via test:lint-rules;
  refuses to run under test-framework globals and counts RuleTester.run
  invocations so a spec that asserts nothing fails instead of passing.
- Correct the ALL_ACTIONS JSDoc in local-actions.token.ts to match
  reality (archive-operation-handler uses LOCAL_ACTIONS).

Reviewed via parallel multi-agent review; findings W1/W2/W4 and a
dangling doc anchor addressed.
2026-05-15 16:51:50 +02:00
johannesjo
ee71f4fe8e docs: restore recognition cues in CLAUDE.md sync rules
Add back the "why/symptom" tails that were over-trimmed: selector-based
effects fire on every store change, multi-entity meta-reducer example,
TODAY_TAG.taskIds ordering-only role, store.dispatch non-blocking
behavior, and SYNC_IMPORT "by design" framing. Rules stay one-liners
but now carry enough context for Claude to recognize when they apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:07:44 +02:00
johannesjo
eddcea7070 docs: trim CLAUDE.md and split E2E reference into e2e/CLAUDE.md
Drop the architecture overview (derivable from code and already
covered in docs/sync-and-op-log/ and ARCHITECTURE-DECISIONS.md),
collapse the long sync invariants into one-liners with pointers,
and dedupe the anti-patterns table against the rule lists. Move
the SuperSync docker-compose block and E2E iteration tips into
e2e/CLAUDE.md where the rest of the E2E reference lives. Tighten
the new docs/documentation-guide.md by dropping content that
duplicates docs/wiki/0.00-Wiki-Structure-and-Organization.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:04:03 +02:00
johannesjo
f8efc5d9c9 docs: require wiki updates for user-facing changes in CLAUDE.md
Add a documentation-update rule to CLAUDE.md and extract the
detailed guidance into docs/documentation-guide.md, mirroring the
styling-guide pattern. Defaults wiki edits to Reference (3.XX) notes
and points to 0.00-Wiki-Structure-and-Organization.md as required
reading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:50:01 +02:00
novikov1337danil
1abb15b804
refactor(backup): unify backup filename format (#7141)
* refactor(backup): implement consistent timestamp for backup filenames

* refactor(i18n): rename PRIVACY_EXPORT key to PRIVACY_EXPORT_DESCRIPTION

* refactor(i18n): add PRIVACY_EXPORT key for data export in multiple languages

* refactor(backup): standardize backup filename prefix

* refactor(backup): update backup filename format to use underscore separator

* refactor(i18n): update translations

* refactor(backup): move getBackupTimestamp utility to a shared location for consistency

* test(backup): add unit tests for getBackupTimestamp utility

* fix(e2e): move MIGRATION_BACKUP_PREFIX to migration.const

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(tests): move jasmine.clock().install() to beforeEach for consistency

* refactor(backup): consolidate backup filename constants and update imports

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 14:09:09 +02:00
Johannes Millan
87f66fea9c refactor(date): centralize startOfNextDay offset behind DateService
Replaces ad-hoc `Date.now() - _dateService.startOfNextDayDiff` arithmetic
scattered across services with a small set of offset-aware helpers on
DateService, and makes the raw field private so TypeScript prevents
future regressions.

Public surface added to DateService:
  - getLogicalTodayDate(): Date — offset-aware "today"
  - getLogicalTomorrowMs(): number — logical today + 1 calendar day,
    DST-safe via setDate (naive +24h stayed on the same local date
    during fall-back transitions)
  - getStartOfNextDayDiffMs(): number — read-only accessor for the
    service-boundary case where pure utilities need the raw offset

The raw `startOfNextDayDiff` field is now `private`; TypeScript
catches both member access and destructuring from outside.

Migrated call sites (all semantic-identical except the DST fix):
  - planner.service.ts: tomorrow$ and ensureDayLoaded
  - add-tasks-for-tomorrow.service.ts: addAllDueToday/Tomorrow
    (+ offset-aware spec coverage)
  - daily-summary.component.ts: isIncludeYesterday
  - global-config.effects.ts + app-state.effects.ts: setTodayString
    payloads now read via the accessor

Preserved for sync-replay determinism:
  - isTodayWithOffset pure util in src/app/util/is-today.util.ts
  - reducers/selectors continue to take startOfNextDayDiffMs as an
    explicit argument from action payloads

Deferred with an in-file TODO: task-due.effects.ts:170 — the effect
is selector-driven (replay-sensitive per CLAUDE.md #8) and its spec is
xdescribe'd, so verification is impossible in this pass.

Documented in CLAUDE.md (Important Development Notes #14 + an
anti-patterns row).

A broader audit surfaced 10 pre-existing sites elsewhere in the
codebase that bypass the logical clock (planner daysToShow$,
argument-less getDbDateStr in pipes, simple-counter streak, plugin
counter). Those are tracked separately and not part of this refactor.
2026-04-17 18:13:02 +02:00
Johannes Millan
772f3f9d97 fix(security): redact user-entered content from remaining log call sites
Remove task titles from reminder dialog trigger, reminder worker
  postMessage, and mobile notification success logs. Add log privacy
  comment to Log class and anti-pattern entry to CLAUDE.md.

  Co-authored-by: Rob Williamson <rob@robwilliamson.com>
2026-03-28 16:35:01 +01:00
Johannes Millan
02dfb9472c docs: add styling guide and reference from CLAUDE.md
Extract styling rules, CSS variable reference, and design token
patterns into docs/styling-guide.md for a consistent design language.
CLAUDE.md now points to the guide for all styling changes.
2026-03-18 13:14:10 +01:00
Johannes Millan
c70ced204e fix(sync): fix stale comments, doc refs, and preserve lastSeq on clean-slate
Remove references to deleted docs/ai/ files, update stale comments about
protectedClientIds and pruning-aware comparison to reflect current REPLACE
semantics, fix MAX=30→20 heading, client_0..29→19 comment, use ?? over ||,
preserve lastSeq in server clean-slate path to prevent sequence reuse, and
add clarifying comments for mock limitations and validation guards.
2026-02-12 16:27:56 +01:00
Johannes Millan
0b73461690 fix(sync): fix vector clock comparison parity and validation consistency
- Fix client/server comparison divergence: remove isVectorClockEmpty
  short-circuit that caused {} vs {a:0} to return LESS_THAN on client
  but EQUAL on shared impl. Now delegates all comparisons to shared.
- Align server value cap with client: raise from 10M to MAX_SAFE_INTEGER
  to prevent silent clock entry stripping on long-term usage.
- Make client validation require integers (Number.isInteger) matching
  server-side validation.
- Add deterministic tie-breaking (localeCompare) to pruning sort for
  equal counter values.
- Fix stale comments: MAX references and CLAUDE.md DoS cap (5x→2.5x).
2026-02-12 16:27:55 +01:00
Johannes Millan
f37280e082 refactor(sync): reduce MAX_VECTOR_CLOCK_SIZE from 30 to 20
Lower the cap to leave headroom for future increases and surface
size-related edge cases earlier. All pruning logic is MAX-agnostic
so this is a safe constant change with documentation updates.
2026-02-12 16:27:55 +01:00
Johannes Millan
29541951a3 refactor(sync): increase MAX_VECTOR_CLOCK_SIZE from 10 to 30 and remove defense layers
At MAX=10, pruning triggered frequently enough (11+ unique client IDs from
reinstalls/new browsers) to require 4 defense layers compensating for
information loss: pruning-aware comparison, protected client IDs with
migration, isLikelyPruningArtifact heuristic, and same-client check.

At MAX=30, pruning almost never triggers (needs 31+ unique client IDs).
A 30-entry clock is ~500 bytes — negligible bandwidth. This allows removing
most defense layers while keeping two cheap backward-compat checks for old
10-entry pruned data still on servers.

Removed:
- Pruning-aware mode in compareVectorClocks (standard comparison now)
- Protected client IDs mechanism (storage, migration, preservation)
- selectProtectedClientIds function
- Clock normalization in SyncImportFilterService

Kept temporarily (backward compat with old 10-entry data):
- isLikelyPruningArtifact with LEGACY_MAX=10
- Same-client check (always mathematically correct)
2026-02-12 16:27:55 +01:00
Johannes Millan
fc56aabec4 docs(sync): fix stale DoS cap references from 3x to 5x MAX_VECTOR_CLOCK_SIZE
The sanitizeVectorClock() DoS cap was changed to 5x MAX (50 entries) but
comments in CLAUDE.md, vector-clocks.md, sync.types.ts, and
validation.service.ts still referenced the old 3x MAX (30) value.
2026-02-10 20:38:32 +01:00
Johannes Millan
a774c46700 fix(sync): remove client-side vector clock pruning from ConflictResolutionService and fix rejection counting
- Remove limitVectorClockSize from ConflictResolutionService (same
  infinite loop risk as SupersededOperationResolverService)
- Include retry-exceeded ops in permanentRejectionCount so sync status
  accurately reflects data loss
- Fix stale JSDoc (100 -> 30 entries) in sanitizeVectorClock
- Remove dead getProtectedClientIds mocks from spec files
- Extract _getEntityKey helper, remove redundant guards, optimize
  Object.keys calls in server pruning log
2026-02-09 17:55:12 +01:00
Johannes Millan
fdc942babb
fix(sync): prevent infinite loop when concurrent modification resolution keeps failing (#6434)
* fix(sync): prevent infinite loop when concurrent modification resolution keeps failing

When vector clock pruning makes it impossible to create a dominating clock
(e.g., entity clock has MAX entries and client ID isn't among them), the cycle
"upload → CONFLICT_CONCURRENT → merge clocks → upload → reject again" repeats
endlessly. This adds a per-entity retry counter (MAX_CONCURRENT_RESOLUTION_ATTEMPTS=3)
that permanently rejects ops after exceeding the limit, breaking the sync loop.

The counter resets when a sync cycle completes with no rejections (healthy state).

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* fix(sync): move vector clock pruning after conflict detection to fix root cause

The infinite sync loop happens because it's mathematically impossible to build
a dominating clock with MAX_VECTOR_CLOCK_SIZE entries when the entity's clock
already has MAX entries and the client's ID isn't among them. The merged clock
needs MAX+1 entries (all entity clock IDs + client ID), but client-side pruning
drops one entity clock ID. The server's pruning-aware comparison then sees the
dropped key as non-shared and returns CONCURRENT instead of GREATER_THAN.

Fix: Move limitVectorClockSize from validation (before comparison) to
processOperation (after comparison, before storage). The full unpruned clock
is now used for conflict detection — all entity clock IDs are present so
bOnlyCount=0 → GREATER_THAN. Storage still gets the pruned clock.

Client-side: Stop pruning in SupersededOperationResolverService. The server
handles pruning after conflict detection.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* fix(sync): tighten vector clock sanitize limit from 100 to 3x MAX_VECTOR_CLOCK_SIZE

The old sanitize cap of 100 entries was unnecessarily wide. Since conflict
resolution clocks are at most ~12-15 entries (entity clock MAX=10 + client ID
+ a few merged), cap at 3x MAX (30) for DoS protection while leaving ample
room for legitimate clocks.

Also update server-side pruning tests to use realistic clock sizes (20 entries
instead of 50) to stay within the new sanitize limit.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* docs(sync): document vector clock pruning invariant and infinite loop fix

- Add "Pruning and the Pruning-Aware Comparison" section to vector-clocks.md
  explaining the critical invariant: server prunes AFTER comparison, not before
- Add rule 13 to CLAUDE.md to prevent future regressions
- Update conflict resolution key files table with rejected-ops-handler and
  superseded-operation-resolver services

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* docs(sync): update last-updated date in conflict resolution docs

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* test(sync): update pruning tests to reflect server-side pruning design

Client no longer prunes vector clocks during conflict resolution — the
server handles pruning after conflict detection. Tests now verify the
merged clock is sent unpruned with all keys preserved.

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

* test(sync): fix client-side pruning tests and add server-side regression test

- Update 7 SupersededOperationResolverService tests to verify client
  does NOT prune (server handles pruning after conflict detection)
- Remove redundant `newClock` alias in superseded-operation-resolver
- Add regression test: MAX+1 entry clock accepted as GREATER_THAN
  when it dominates a MAX entry entity clock (the core infinite loop fix)

https://claude.ai/code/session_016RAxms5dTouU98wFNQaAcv

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-09 12:01:43 +01:00
Johannes Millan
200525c752 docs: clarify commit message convention for test changes
Reserve `fix` type for actual code/bug fixes. Test-related changes
should always use `test:`, e.g., `test(e2e):` not `fix(e2e):`.
2026-02-08 14:16:06 +01:00
Johannes Millan
702f768784 docs: clarify test commit message convention 2026-01-21 14:34:45 +01:00
Johannes Millan
038a722ed8 style: apply prettier formatting 2026-01-10 17:09:14 +01:00
Johannes Millan
74fe0c01a0 Merge branch 'master' into feat/operation-logs
* master:
  16.9.4
  docs: add Angular commit message guidelines to CLAUDE.md
  fix(ci): update references for repo migration to super-productivity org
  build(ci): improve android build release upload reliability
  16.9.3
  fix(calendar): display proper error messages instead of [object Object]
  fix(e2e): fix keyboard shortcut delete test and reduce sync test flakiness
  fix(tasks): add subscription cleanup to delete confirmation dialog
  fix(schedule): maintain visibility during task overlap on schedule (#5887)
  16.9.2

# Conflicts:
#	e2e/pages/sync.page.ts
2026-01-10 13:06:20 +01:00
Johannes Millan
7179e3d6f8 docs: add Angular commit message guidelines to CLAUDE.md 2026-01-10 12:57:32 +01:00
Johannes Millan
cdf52cc4e5 fix(e2e): split docker-compose files for standalone webdav tests
docker-compose.e2e.yaml was failing when used standalone because the
supersync service only had a port override without a base definition.
Split into separate files so e2e:docker:webdav works without errors.
2026-01-09 17:27:30 +01:00
Johannes Millan
dd79f54c23 fix: address code review findings from pfapi-to-oplog refactor
- Update CLAUDE.md to reference /src/app/op-log/persistence/ instead
  of deleted /src/app/pfapi/ directory
- Fix boolean coercion in archive-migration.service.ts (same pattern
  as bug fixed in 183bf2c18 for LegacyPfDbService)
- Replace deprecated .toPromise() with firstValueFrom() in
  archive.service.ts for RxJS 8 compatibility
2026-01-07 16:21:18 +01:00
Johannes Millan
3f3f0685eb Merge branch 'master' into feat/operation-logs
* master: (21 commits)
  test: increase timeout for encryption
  16.8.3
  fix(e2e): use pressSequentially for time input in task-detail tests
  fix(sync): resolve 25-second initial sync timeout race condition
  feat(e2e): add Docker-based E2E test isolation
  fix(schedule): start tracking selected task when pressing Y in schedule view
  fix(tasks): clear reminder when clicking "today" button on already-today tasks
  fix(e2e): use format-agnostic time change in task-detail tests
  16.8.2
  fix(test): reset selector overrides to prevent test pollution
  build: update CLAUDE.md
  fix(calendar): add periodic refresh for planner and scheduler views
  feat(effects): consolidate task update actions in PluginHooksEffects
  fix(sync): show skip button immediately when offline
  fix(db): add missing _afterReady guard to loadAll method
  feat(android): add alarm sound and vibration to task reminders
  feat(sync): add skip button to loading screen when waiting for sync
  fix(pomodoro): allow manual session end to start break early
  fix(i18n): add missing translate pipe to play button tooltip
  fix(tasks): handle undefined tasks in reminder effect
  ...

# Conflicts:
#	CLAUDE.md
#	docker-compose.e2e.yaml
#	e2e/tests/task-detail/task-detail.spec.ts
#	package.json
#	src/app/features/tasks/store/task-reminder.effects.spec.ts
#	src/app/features/tasks/store/task-reminder.effects.ts
#	src/app/plugins/plugin-hooks.effects.ts
2026-01-04 18:20:10 +01:00
Johannes Millan
d8a6a7dc62 build: update CLAUDE.md 2026-01-04 14:52:35 +01:00
Johannes Millan
016e680c5f fix(sync): fix simple counter sync and quota exceeded handling
- Add null checks in simple-counter.reducer.ts to prevent crashes when
  entity doesn't exist (fixes "Cannot read properties of undefined")
- Add missing entityIds to updateAllSimpleCounters action - was causing
  operation log to skip persistence
- Add try-catch in operation-log-upload.service.ts to show alert when
  storage quota exceeded (413 error)
- Fix supersync-network-failure.spec.ts test timing - route interception
  must happen before task creation
- Add documentation to CLAUDE.md for running supersync tests with
  real-time output using --reporter=line
2025-12-29 17:09:46 +01:00
Johannes Millan
8fff1325b4 docs: fix SYNC_IMPORT filtering section to match implementation
- Fix CLAUDE.md path reference from docs/op-log/ to docs/sync-and-op-log/
- Rewrite section 2c in architecture diagrams to reflect actual implementation:
  - Was: UUIDv7 timestamp replay (removed feature)
  - Now: Vector clock filtering with clean slate semantics
- Update service reference from removed _replayLocalSyncedOpsAfterImport()
  to SyncImportFilterService.filterOpsInvalidatedBySyncImport()
- Clarify that CONCURRENT ops are dropped, not replayed
2025-12-27 13:15:47 +01:00
Johannes Millan
b4ce9d5da6 docs: reorganize sync and operation-log documentation
Move scattered architecture docs into centralized locations:

- Move operation-log docs from src/app/core/persistence/operation-log/docs/
  to docs/op-log/
- Flatten docs/sync/sync/ nested structure to docs/sync/
- Move supersync-encryption-architecture.md from docs/ai/ to docs/sync/
- Copy pfapi sync README to docs/sync/pfapi-sync-overview.md
- Update all cross-references to use new paths

This improves discoverability and keeps architecture documentation
separate from source code.
2025-12-27 10:54:13 +01:00
Johannes Millan
3a390f5e59 docs(claude): update E2E testing instructions for improved efficiency 2025-12-26 18:45:33 +01:00
Johannes Millan
9f6bb4e7ac fix(sync): implement clean slate semantics for SYNC_IMPORT/BACKUP_IMPORT
SYNC_IMPORT and BACKUP_IMPORT now represent a complete fresh start:
- All operations without knowledge of the import are dropped
- CONCURRENT ops from "unknown clients" are no longer preserved
- Local synced ops are NOT replayed after import

This ensures a true "restore to point in time" semantic where all
clients start fresh from the imported state, with no concurrent
work preserved.

Changes:
- Simplify SyncImportFilterService to filter ALL CONCURRENT/LESS_THAN ops
- Remove _replayLocalSyncedOpsAfterImport() method and related code
- Remove _checkOperationEntitiesExist() helper method
- Update tests to expect new behavior
- Document semantics in CLAUDE.md
2025-12-26 11:20:05 +01:00
Johannes Millan
d48694d0ce feat(docs): add command for running SuperSync E2E tests 2025-12-24 14:15:29 +01:00
Johannes Millan
7f24d13bd6 docs: add event loop yield pattern to development notes
Document the pattern of yielding to the event loop after bulk NgRx
dispatches to prevent state updates from being lost during rapid
dispatch sequences (50+ operations).
2025-12-22 12:07:44 +01:00
Johannes Millan
ee6a766a46 fix(sync): add vector-clock dominance check to late-joiner replay
- Add missing vector-clock comparison in _replayLocalSyncedOpsAfterImport
  that filters ops dominated (LESS_THAN) by SYNC_IMPORT's clock
- Replace local MAX_REJECTED_OPS_BEFORE_WARNING with imported constant
- Fix CLAUDE.md doc path reference to operation-log-architecture-diagrams.md
- Add 6 comprehensive tests for vector-clock dominance behavior
2025-12-12 20:48:40 +01:00
Johannes Millan
149947b8ae fix(sync): suppress selector-based effects during hydration/replay
Selector-based effects (like preventParentAndSubTaskInTodayList$) were
firing during operation replay because they subscribe directly to store
selectors rather than actions. LOCAL_ACTIONS filtering only works for
action-based effects.

This caused duplicate operations to be captured during hydration, growing
the operation queue unnecessarily and impacting performance.

- Add HydrationStateService to track when remote ops are being applied
- Wrap OperationApplierService.applyOperations() with hydration state
- Filter preventParentAndSubTaskInTodayList$ during hydration
- Document selector-based effects anti-pattern in CLAUDE.md
2025-12-12 20:48:13 +01:00
Johannes Millan
d3061fc15b docs: add effects anti-pattern to CLAUDE.md
Highlight the rule that effects should NEVER run for remote operations
in the anti-patterns section for better visibility.
2025-12-12 20:47:48 +01:00
Johannes Millan
3e8242bc55 refactor(sync): add ArchiveOperationHandler for remote archive ops
Establish general rule: effects should NEVER run for remote operations.
Side effects happen exactly once on the originating client.

- Create ArchiveOperationHandler service for remote archive side effects
- Integrate handler into OperationApplierService (called after dispatch)
- Change archive.effects.ts to use LOCAL_ACTIONS only
- Update CLAUDE.md with the general rule about effects
- Add Section 8 to architecture diagrams documenting the pattern

For archive operations:
- moveToArchive: local writes BEFORE dispatch, remote via handler AFTER
- restoreTask: local via effect, remote via handler
- flushYoungToOld: local via effect, remote via handler
2025-12-12 20:47:48 +01:00
Johannes Millan
78c65acf4d test(e2e): add sd:today to tasks for TODAY view visibility
Tasks created without explicit dueDay don't appear in TODAY view
due to recent selector changes. Add sd:today short syntax to ensure
tasks have dueDay set and appear correctly in tests.
2025-12-12 20:47:48 +01:00
Johannes Millan
02323dd33d docs: add atomic multi-entity changes guideline to CLAUDE.md
Add note #8 explaining when to use meta-reducers instead of effects
for multi-entity state changes to ensure sync consistency.
2025-12-12 20:47:48 +01:00
Johannes Millan
04a9ef1e34 refactor(sync): migrate move operations to anchor-based positioning
Replace newOrderedIds (full list) with afterTaskId (anchor) for sync-friendly
drag-drop operations. Concurrent moves now merge correctly instead of
conflicting.

Actions migrated:
- moveProjectTaskInBacklogList
- moveProjectTaskToBacklogList
- moveProjectTaskToRegularList
- moveTaskInTodayList (PROJECT and TAG contexts)

Also includes:
- deleteProject payload slim-down (projectId + noteIds instead of full Project)
- Helper functions: moveItemAfterAnchor(), getAnchorFromDragDrop()
- Comprehensive tests for all anchor-based moves
- Documentation updates in todo.md
2025-12-12 20:46:41 +01:00
Johannes Millan
ac8ada84f0 fix(op-log): add error handling for critical failure paths
- Add clientId validation before persisting operations
- Track compaction failures and notify user after 3 consecutive failures
- Add hydration recovery notification when both hydration and recovery fail
- Wrap conflict resolution in try-catch for atomicity
- Add HYDRATION_FAILED, COMPACTION_FAILED, CONFLICT_RESOLUTION_FAILED translations
- Update CLAUDE.md: only edit en.json for translations
2025-12-12 20:46:21 +01:00
Johannes Millan
f8e0160801 docs: plan operation logs again 1 2025-12-12 20:46:05 +01:00
Johannes Millan
9f2c786e41 refactor: move tests 2025-08-02 11:37:25 +02:00
Johannes Millan
41b287fd9d refactor(e2e): simplify Playwright commands to essentials
- Keep only the most useful commands:
  - e2e:playwright - run all tests with minimal output
  - e2e:playwright:file - run single file with detailed output
  - e2e:playwright:ui/debug/headed/report - existing useful commands
- Remove complexity: test-summary.js, minimal config, redundant commands
- Use line reporter by default for cleaner output
- Update CLAUDE.md documentation

BREAKING CHANGE: Removed e2e:playwright:quick, e2e:playwright:failures, and e2e:playwright:summary commands
2025-08-01 18:44:33 +02:00
Johannes Millan
fadff3bc19 test(e2e): add command to run single test file
- Add npm run e2e:playwright:file command for running individual test files
- Update CLAUDE.md documentation with all Playwright commands
- Example usage: npm run e2e:playwright:file tests/work-view/work-view.spec.ts
2025-08-01 18:44:33 +02:00