Commit graph

1723 commits

Author SHA1 Message Date
Johannes Millan
350536e7ed 18.12.0 2026-06-20 17:17:50 +02:00
Johannes Millan
2cf67cee71 18.11.0 2026-06-18 13:37:47 +02:00
Johannes Millan
5497212b99 fix(android): inset WebView for IME under enforced edge-to-edge (draft)
Durable follow-up to the #8295 revert. On targetSdk 36 (Android 16) edge-to-edge
is mandatory and adjustResize is a no-op for the IME, so the WebView only stays
above the soft keyboard if @capawesome/capacitor-android-edge-to-edge-support
insets it. The plugin instead zeroes the WebView bottom margin while the keyboard
is visible (assuming the system resized the window), leaving fixed content behind
the IME.

patch-package the plugin's EdgeToEdge.applyInsetsInternal so the WebView bottom
margin is always max(imeInsets.bottom, systemBarsInsets.bottom). The WebView then
shrinks above the keyboard, content resizes, and --keyboard-height stays 0 with
the add-task bar above the IME on the existing JS path. Wire patch-package via a
postinstall script; run 'npm install' to apply (also refreshes the lockfile).

DRAFT: verified the patch applies cleanly, but the gradle build and on-device
behavior are UNVERIFIED here. Needs the Android 10/14/15/16 matrix in
docs/android-edge-to-edge-keyboard.md before merge; upstream so the patch can be
dropped.
2026-06-16 17:00:55 +02:00
Johannes Millan
f51954f805
fix(deps): bump @angular/* to 21.2.17 and tmp to 0.2.7 (security) (#8431)
Clears the open Dependabot security advisories for the Angular runtime packages and tmp.

The whole @angular/* framework family is bumped together (21.2.14 -> 21.2.17): the packages use exact-version peer deps, so bumping @angular/core alone (as #8409 did) leaves the siblings at 21.2.14 and fails 'npm ci' with an ERESOLVE peer conflict. Supersedes #8409.
2026-06-16 14:25:02 +02:00
Johannes Millan
5d2abbf847 18.10.0 2026-06-12 13:11:09 +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
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
johannesjo
1f24d5572d 18.9.1 2026-06-05 22:45:35 +02:00
Johannes Millan
2aacba1c98
fix(ci): repair v18.9.0 release pipeline (MAS build, iOS submit, flaky lock test) (#8040)
* fix(build): keep @electron/asar on minimatch v3 for MAS universal build

The `overrides.app-builder-lib.minimatch` pin to v10 cascaded into
@electron/asar (a child of both app-builder-lib and @electron/universal),
which only works with minimatch v3. minimatch v9/v10 ship as ESM with
`__esModule: true` and no `default` export, so @electron/asar's compiled
default import (`minimatch_1.default(...)`) resolved to undefined and threw
`TypeError: (0 , minimatch_1.default) is not a function` during the
universal-app asar merge (makeUniversalApp -> mergeASARs -> shouldUnpackPath),
failing `dist:mac:mas:buildOnly` in the Mac Store release workflow.

Carve @electron/asar back to minimatch 3.1.2 via nested overrides while
app-builder-lib (and @electron/universal) keep minimatch v10 as intended.

* fix(ci): declare export compliance for iOS App Store submission

The iOS release build, upload and processing all succeed, but
`upload_to_app_store` fails at submit-for-review with:

  [!] Export compliance is required to submit
      Example: submission_information: { export_compliance_uses_encryption: false }

Super Productivity only relies on exempt encryption (HTTPS / standard OS
crypto), so declare that the app does not use non-exempt encryption:

- ios/App/App/Info.plist: ITSAppUsesNonExemptEncryption=false (canonical,
  build-time declaration; also covers TestFlight, auto-resolved by ASC).
- fastlane/Fastfile: export_compliance_uses_encryption: false in
  submission_information, so the submit-for-review API call carries the
  declaration regardless of the binary.

* refactor(build): simplify @electron/asar minimatch override to top-level

Follow-up to the minimatch carve-out. Hoisting a top-level
`overrides["@electron/asar"].minimatch` pin produces a byte-identical
package-lock.json to the previous nested form, but is simpler and covers
every @electron/asar consumer (app-builder-lib, @electron/universal,
electron-winstaller) regardless of which parent wins hoisting — rather than
relying on a nested branch under app-builder-lib (whose @electron/universal
sub-branch was dead config given asar dedupes to a single instance).

* test(op-log): de-flake LockService mutex-invariant timeout test

"should preserve mutex invariant after timeout" flaked on the macOS CI
runner (TZ=America/Los_Angeles leg): it asserted that C always times out
waiting for the lock, but on a slow/loaded runner A can release the lock
before C's 50ms timeout fires, so C legitimately acquires it and runs —
after A has finished. The observed `['a-start','a-end','c-start']` actually
satisfies the invariant (C ran after A, never concurrently); only the
brittle "C must time out" assertion failed.

Assert the real no-concurrent-execution invariant instead: C must never
start before A ends. This still catches the original regression (concurrent
C would push 'c-start' before 'a-end') but is independent of runner timing.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 22:26:42 +02:00
Johannes Millan
920071f5ce 18.9.0 2026-06-05 20:30:11 +02:00
Johannes Millan
54671f87be
chore(deps): resolve 30 Dependabot security alerts (dev/build tooling) (#7960)
* chore(deps): bump plugin-dev dev tooling to patch security alerts

Update vite, vitest, postcss, glob, and minimatch across the
packages/plugin-dev/* sample plugins to clear 26 Dependabot alerts.
These are dev/build tooling for the example plugins and are not
shipped to end users.

- vite >=7.3.2, vitest 4.1.x, postcss 8.5.15, glob 10.5.0,
  minimatch 9.0.9/5.1.9 (all within declared semver ranges)
- clickup-issue-provider: vitest ^3.2.1 -> ^4.1.0 (only manifest change)

Verified test suites pass: clickup (28), automations (109).

* chore(deps): bump minimatch + webpack-dev-server overrides for security

Resolve the remaining root-lockfile Dependabot alerts in build/dev
tooling (not shipped to end users):

- minimatch: the existing `app-builder-lib` override pinned minimatch to
  10.1.1, which npm cascades across electron-builder's entire subtree
  (@electron/asar, @electron/universal, dir-compare, filelist, temp).
  Bump the override to 10.2.5 (within app-builder-lib's ^10.0.3); the
  vulnerable nested copies dedupe to the patched top-level. Clears
  Dependabot #341, #372, #373 (minimatch ReDoS).
- webpack-dev-server: add override to 5.2.4. Clears #593.

Note: electron-builder `npm run dist` packaging not run in this env;
the minimatch bump is a patch within range so impact is expected nil.
2026-06-02 19:44:21 +02:00
Johannes Millan
63d46f7a0b
feat(op-log): validate SQLite backend + IDB→SQLite migration + backend-aware init (#7931) (#7954)
* test(op-log): validate SqliteOpLogAdapter against a real sql.js engine

The 23 adapter specs ran only against an in-memory regex stand-in that
models the SQL shapes the adapter emits — it validates the translation
layer, not SQLite itself. Add sql.js (dev-only; never in the app bundle)
served into Karma, and run the behavioral contract against BOTH the fake
and a real SQLite engine.

This exercises genuine-engine behavior the stand-in could only model:
the real UNIQUE-constraint message -> ConstraintError mapping,
AUTOINCREMENT never reusing seq after clear(), compound-index + NULL
range handling, and real BEGIN IMMEDIATE rollback. 51/51 green.

B2 (translation-layer pass) per docs/sync-and-op-log/sqlite-migration.md.
The integration-harness second pass and the on-device real-engine run
remain.

* test(op-log): run store-port integration against real sql.js (B2 stage 2)

Parameterize the RemoteOperationApplyStorePort integration scenarios to
run against BOTH the default IndexedDB backend and a sql.js-backed
SqliteOpLogAdapter, exercising the store's COMPOSED flows (apply/mark/
merge-clock, partial-failure persistence, full-state import clearing,
vector-clock persistence) on a real SQL engine — not just the adapter in
isolation. 6/6 green.

Surfaced a real B3 wiring gap: OperationLogStoreService.init() is
IDB-shaped (opens+adopts an IndexedDB connection, never calls the
adapter's own init()). For a self-managing backend like SQLite the
tables would not exist. The sql.js setup creates them once on the shared
db to mirror the store-init change B3 must make on native (call
adapter.init() / skip the IDB open when the backend is SQLite).

* feat(op-log): add verified IDB->SQLite backend migration (C1)

One-time copy of the entire op-log from a source adapter (legacy
IndexedDB) to a dest adapter (SQLite) in a single dest transaction with
verify-before-commit: a mismatch in op count, last seq, or vector clock
throws and rolls the dest back, leaving it empty and the source
untouched.

Adapter-agnostic (talks only to the OpLogDbAdapter port), so it is
validated in CI with a real Chrome IndexedDB source + a sql.js SQLite
dest; the native @capacitor-community/sqlite dest behaves identically
through the same port. The generic iterate->put copy preserves ops seq
(incl. gaps) via the put-honors-seq path and writes singletons at their
out-of-line key uniformly, with no per-store special-casing.

Not wired into startup — Phase B3/C2 decide WHEN to run it (SQLite empty
+ legacy SUP_OPS present) and retain the IDB copy >= 1 release. 5/5
green, incl. seq-fidelity, AUTOINCREMENT-continues-past-migrated,
empty-source, non-empty-dest guard, and verify-rollback.

* docs(sync): record sql.js validation, C1, and the B1/B3 findings

Update the SQLite migration plan + follow-up backlog to reflect what
landed this pass and hand off the device-gated remainder:
- B2: real-engine (sql.js) adapter contract + store-port second pass are
  done in CI; only the on-device run remains.
- C1: the backend-migration algorithm + verify-before-commit are done
  and tested (real IDB -> sql.js); only the startup wiring remains.
- B3 finding: OperationLogStoreService.init() is IDB-shaped (opens+adopts
  IDB, never calls adapter.init()); native must call adapter.init() and
  skip the IDB open on SQLite.
- B1 perf note: bridge round-trips dominate on native; return lastId from
  the plugin's run response and add a runBatch/executeSet bulk path so
  appendBatch is one crossing, with RETURNING-seq for per-op seq.

* feat(op-log): make store init backend-aware for self-managing backends (B3)

OperationLogStoreService.init() and ArchiveStoreService._init() were
IDB-shaped: they unconditionally opened+adopted a WebView IndexedDB
connection and never called the adapter's own init(). For a self-managing
backend (SQLite) that meant (a) the adapter's tables were never created
and (b) it still touched the evictable WebView store this migration
exists to escape.

Now: when the adapter exposes no adoptConnection (i.e. it self-manages,
like SQLite), call adapter.init() and skip the IndexedDB open. The
adopt-connection (IndexedDB) path is unchanged. The new branch is dead in
production until B3 flips the native token, so this is risk-free now and
unblocks that flip.

Tested: two unit tests cover both branches (self-managing -> adapter.init,
no IDB open; IDB -> open+adopt, no adapter.init). The store-port
integration spec now drives the store fully on SQLite with _db undefined,
so its earlier pre-init workaround is removed. 521 persistence + 6
integration green.

* docs(sync): mark the B3 backend-aware init fix as landed

The store-init half of B3 (call adapter.init() / skip the IDB open for
self-managing backends) is implemented + CI-tested; only the device-gated
native token flip + SqliteDb wrapper remain.
2026-06-02 17:26:23 +02:00
dependabot[bot]
fc9321b7e3
chore(deps): bump @schematics/angular from 21.2.12 to 21.2.13 (#7936)
Bumps [@schematics/angular](https://github.com/angular/angular-cli) from 21.2.12 to 21.2.13.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v21.2.12...v21.2.13)

---
updated-dependencies:
- dependency-name: "@schematics/angular"
  dependency-version: 21.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 14:14:52 +02:00
dependabot[bot]
d96a543dd2
chore(deps): bump @material-symbols/font-400 from 0.44.9 to 0.44.10 (#7934)
Bumps [@material-symbols/font-400](https://github.com/marella/material-symbols/tree/HEAD/font/400) from 0.44.9 to 0.44.10.
- [Release notes](https://github.com/marella/material-symbols/releases)
- [Commits](https://github.com/marella/material-symbols/commits/v0.44.10/font/400)

---
updated-dependencies:
- dependency-name: "@material-symbols/font-400"
  dependency-version: 0.44.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 14:13:07 +02:00
Johannes Millan
24725c9360 18.8.0 2026-05-30 01:19:30 +02:00
Johannes Millan
fa8861c486 test: install fake-indexeddb to isolate Karma specs from real Chrome IDB
The shared Karma Chrome instance kept disconnecting mid-suite with
"executing a cancelled action" cascades — rxjs scheduler errors that
trace back to leftover IndexedDB connections, version-change races, and
open IDBPDatabase handles held by providedIn:'root' services that
TestBed does not destroy between specs (#7800 CI rerun, attempt 2).

Switch unit specs to fake-indexeddb/auto and swap globalThis.indexedDB
to a fresh IDBFactory in a global beforeEach so each spec starts on an
empty in-memory database. The class globals from /auto stay constant so
the idb library's instanceof checks still pass, while the per-factory
_databases map being empty means no cross-spec state can leak.

The four connection-lifecycle handler tests in operation-log-store and
archive-store specs dispatched synthetic versionchange/close events to
verify the service reopens on browser-side close. fake-indexeddb does
not faithfully reproduce that post-close reopen path, and we cannot
swap real-Chrome IDB back in mid-suite because idb captures
IDBOpenDBRequest etc. by reference at module load. Mark them xit with
a FIXME noting the two paths forward (refactor handlers to named
methods, or a separate Karma config without the polyfill).
2026-05-27 00:39:57 +02:00
Johannes Millan
a4487e40a3
fix(android): restore IME inset handling via edge-to-edge plugin (#7765)
The Capacitor 8 migration (334b14aa2) dropped `adjustMarginsForEdgeToEdge:
'auto'` from `capacitor.config.ts`. On targetSdk 36 (Android 16) edge-to-edge
is mandatory and the WebView no longer applies IME insets automatically —
position: fixed elements (the global add-task-bar in particular) end up
anchored to a layout viewport that doesn't shrink when the keyboard opens,
so they appear to scroll with content swipes and jump inconsistently during
the IME show/hide animation.

Pull in `@capawesome/capacitor-android-edge-to-edge-support` as the
structural replacement: it registers a single `ViewCompat.setOnApplyWindow
InsetsListener` that applies system-bar + display-cutout insets to the
WebView and defers IME to `windowSoftInputMode="adjustResize"` (skipping its
own bottom margin while the keyboard is visible to avoid double-counting).
Combined with `SystemBars.insetsHandling: 'disable'` (required by the plugin
to prevent Capacitor's core insets handler from fighting it) and
`Keyboard.resizeOnFullScreen: false` (the plugin's recommended setting; a
no-op on iOS where this key doesn't apply, and the Keyboard plugin is
excluded from Android via `includePlugins` anyway), this restores the
pre-migration WebView sizing behaviour.

No JS changes — the plugin self-installs once present.
2026-05-26 13:27:56 +02:00
Johannes Millan
334b14aa26 feat: migrate to capacitor 8
- fix(sync): unblock @sp/sync-core/@sp/sync-providers typecheck under vitest 4
- chore(mobile): pin @capacitor/keyboard to 8.0.1
- chore(mobile): migrate to Capacitor 8
- Merge branch 'feat/issue-7749-3341d1'
- docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
- test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
- chore(plugins): re-bundle document-mode and document Stage A path
2026-05-23 20:42:00 +02:00
Johannes Millan
77f83c5687
test(e2e): harden failure signals and provider gates (#7753)
* test: harden e2e failure signals

Fail otherwise-passing E2E tests on browser runtime errors, keep Playwright retries disabled, preserve Docker E2E exit codes, and make plugin/WebDAV setup failures hard failures instead of logged or skipped conditions.

* test: harden provider e2e runners

Make WebDAV and SuperSync runner scripts require provider readiness, preserve cleanup and argument forwarding, and fail manual sync clients on uncaught page errors.

* ci: require providers for scheduled e2e

Set required-provider flags in scheduled WebDAV and SuperSync jobs, and remove the duplicate provider runner scripts while keeping local npm aliases inline.

* test: catch e2e teardown pageerrors and tighten fixture

- closeClient now asserts runtime errors AFTER context.close() so
  pageerrors emitted during teardown (Angular destroy hooks, late RxJS
  errors) are captured instead of dropped. Matches the pattern in
  guardContextCloseWithRuntimeErrorCheck.
- test.fixture.ts isolatedContext now spreads Playwright's merged
  contextOptions instead of destructuring 23 fields by hand. Future
  option additions propagate automatically; the page fixture uses the
  shared attachPageErrorCollector and only fails on pageerror (not
  console.error, which is too noisy). Guards against a configured 0
  timeout being treated as undefined.
- plugin-loading.spec.ts second test now hard-asserts that the plugin
  menu entry reappears after re-enable, matching the first test instead
  of silently logging when not visible.

* test(sync): stabilize ImmediateUploadService spec

Two complementary fixes for flaky failures observed under full-suite
random-order runs where the upload pipeline silently never fires:

- Pin navigator.onLine = true in beforeEach (restored in afterEach).
  isOnline() inside _canUpload reads navigator.onLine directly. The
  keyboard-layout spec replaces the whole navigator and the is-online
  spec spies on it; if order or restoration ever drifts, every "should
  fire upload" test fails trivially while the "should NOT" tests pass.
- Replace tick(2100) with tick(2000); flush(). The await chain inside
  withSession() (provider.isReady, withSession entry, uploadPendingOps,
  optional LWW re-upload) requires more microtask drain than tick's
  fixed-time window reliably provides under load. flush() drains the
  pipeline regardless.

* test(e2e): guard skipOnboarding init script against data: frames

The new page-error collector started failing plugin specs because
addInitScript runs in every frame — including the empty data:text/html
iframe that plugin-index swaps in on destroy — and localStorage access
in a data: URL throws SecurityError. Wrap the four setItem calls in
try/catch so the helper noops in storage-less frames.
2026-05-23 20:33:04 +02:00
Johannes Millan
4212ed4b0d 18.7.0 2026-05-23 12:53:32 +02:00
Johannes Millan
61d5cba934 fix(build): remove sharp from deps to fix F-Droid build
sharp is a native module that fails in F-Droid's offline/restricted
build environment. The prior partial fix only moved it to
optionalDependencies but left it in package-lock.json, so the build
still pulled it in. Remove sharp from package.json and the lockfile
entirely (replicating the proven #6637 fix), and lazy-install it on
demand only in the dev-only marketing-screenshot scripts that need it.

Closes #7542
2026-05-18 21:59:24 +02:00
johannesjo
605e5f6d52 18.6.0 2026-05-16 14:26:17 +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
Johannes Millan
e4f9a4e2e5 refactor(sync-providers): extract local file provider 2026-05-13 11:36:19 +02:00
Johannes Millan
a97a15457b refactor(sync-providers): scaffold provider package 2026-05-12 19:14:12 +02:00
Johannes Millan
075de5a097 feat(e2e): add keyboard, mobile, and shorts video variants
Three new REEL_VARIANT branches for the marketing video pipeline:

- shorts: 9:16 portrait 1080x1920 for TikTok / YouTube Shorts /
  Instagram Reels. Skips the side-panel drag beat (no horizontal room)
  and tightens holds for portrait capture.
- keyboard: keyboard-first reel demonstrating SP shortcuts. Five beats
  driven by real page.keyboard.press() so cause-and-effect is honest
  (Shift+A capture, J/K navigate, F focus mode).
- mobile: 19.5:9 phone aspect 1080x2340 with hasTouch + isMobile UA.
  Fixture installs a tap-ripple on touchstart/pointerdown in lieu of
  the cursor highlight; tap-driven choreography.

Fixture gains a getVideoProfile() selector that returns the right
viewport, DPR, and touch flags per variant. Overlays grow new
primitives needed by the keyboard and mobile choreographies.
2026-05-12 17:57:30 +02:00
Johannes Millan
f273a75e6f feat(video): add Microsoft Store trailer variant 2026-05-11 16:46:04 +02:00
Johannes Millan
9fd9d386a8 refactor(sync): move vector clocks to sync-core 2026-05-11 15:21:08 +02:00
Johannes Millan
4534789571 feat(screenshots): automate flathub output 2026-05-11 13:35:17 +02:00
Johannes Millan
2b28b5a253 fix(build): make sharp optional dependency 2026-05-11 10:09:15 +02:00
Johannes Millan
5fc9fe0411
refactor(sync): extract framework-agnostic sync types into @sp/sync-core (#7546)
* refactor(sync): extract framework-agnostic sync types into @sp/sync-core

Stand up packages/sync-core/ as the new home for sync types and
constants that have no Angular/NgRx coupling. This is the thin first
slice of separating sync engine, configuration, and provider concerns
into distinct packages.

Files moved (sources now live in packages/sync-core/src/):
- core/operation.types.ts
- core/action-types.enum.ts
- core/lww-update-action-types.ts
- core/sync-state-corrupted.error.ts
- core/types/apply.types.ts
- sync-providers/provider.const.ts
- util/entity-key.util.ts

Original paths in src/app/op-log/ keep working as thin re-export stubs
so existing callers don't change. op-log/sync-exports.ts now sources
provider.const exports directly from @sp/sync-core.

Files with transitive Angular dependencies (encryption, sync-errors,
provider.interface, vector-clock util) stay in the app for now — moving
them requires introducing a logger port and is part of the next slice.

* refactor(sync): keep @sp/sync-core domain-agnostic

The first slice landed too much Super Productivity-specific content in
@sp/sync-core. The lib should expose generic sync primitives; the host
app supplies the SP-specific config.

Pulled out of the lib (now app-side only):
- ActionType enum (NgRx action strings for SP features)
- ENTITY_TYPES / EntityType union (SP domain entities)
- SyncImportReason union (SP import flows)
- RepairSummary / RepairPayload (SP repair shape)
- WrappedFullStatePayload + appDataComplete helpers (SP wire format)
- SyncProviderId / SyncStatus / ConflictReason / OAUTH_SYNC_PROVIDERS
  / REMOTE_FILE_CONTENT_PREFIX / PRIVATE_CFG_PREFIX (SP providers/keys)
- The @sp/shared-schema dep (also SP-coupled)

Generic-ized in the lib:
- Operation.actionType: string and Operation.entityType: string (lib
  carries opaque strings; host narrows in app code)
- Operation.syncImportReason removed; app extends Operation with it
- VectorClock now defined locally as Record<string, number>
- LWW helpers replaced by createLwwUpdateActionTypeHelpers(entityTypes)
  factory; the app instantiates it with SP's ENTITY_TYPES
- entity-key.util uses string for entityType

App stubs now redeclare the SP-narrowed Operation, EntityChange,
EntityConflict, ConflictResult, MultiEntityPayload, ApplyOperationsResult
on top of the lib generic types via Omit-and-extend, and re-host all the
SP-specific helpers (WrappedFullStatePayload, extractFullStateFromPayload,
assertValidFullStatePayload, RepairSummary, RepairPayload).

Also added @sp/sync-core to src/tsconfig.spec.json paths so the spec
build uses the source, not the dist (matching shared-schema).

* docs(sync): add @sp/sync-core extraction plan

Roadmap for carving the sync engine out of src/app/op-log/ into a
reusable, framework-agnostic AND domain-agnostic @sp/sync-core package
plus a sibling @sp/sync-providers.

Documents:
- The three-concern split (engine / config / providers) target
- The domain rule: nothing SP-specific lands in the lib (ActionType,
  ENTITY_TYPES, SyncImportReason, RepairPayload, SyncProviderId, the
  appDataComplete wire format, @sp/shared-schema all stay app-side)
- PR 1 (landed): generic primitives only; app stubs preserve every
  pre-existing call site via Omit-and-extend
- PR 2: SyncLogger port + parameterize entity-registry
- PR 3a: pure algorithmic core (vector-clock client wrapper, conflict
  detection, op validation, encryption, sync-errors) — needs only the
  SyncLogger port
- PR 3b: orchestrators behind ports (OperationStorePort,
  ActionDispatchPort, ConflictUiPort, SyncConfigPort) — the high-risk
  step where the app/lib boundary becomes load-bearing
- PR 4: lift providers into @sp/sync-providers
- PR 5: ESLint boundary rule

* refactor(sync): address sync-core extraction review

* docs(sync): refine extraction plan follow-ups

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 23:23:36 +02:00
johannesjo
1fc048df61 chore(video): smooth marketing reel output
Note: the drag/drop reel works finally with the native app drag preview path.
2026-05-10 01:22:06 +02:00
johannesjo
17e664f2fc build(release): add automated release notes 2026-05-10 00:24:47 +02:00
Johannes Millan
2b06d0b886 18.5.0 2026-05-09 20:27:05 +02:00
Johannes Millan
6708d133fc fix(restore): undo collateral reverts from 1d52843c
The "feat(video): Playwright-driven marketing reel pipeline" commit
1d52843c was rebased on a stale base and silently reverted unrelated
work landed earlier the same day. The follow-up 3acdcd47 only
restored Lines and Velvet themes; this commit restores the rest.

Restored:
- task.service.ts focusTaskById prefers in-panel instance again (#7120
  mobile sub-task focus regression)
- Schedule responsive header: BREAKPOINTS.XS/XXS, WEEK_NR_SHORT
  translation key, _isCompact/_isVeryCompact computeds
- Calendar visibility mat-menu form (fff1ee15) instead of the older
  mat-chip-listbox bar
- import.page.ts no_dialog fallback + second-chance importCompletePromise
- sharp devDependency (build-store-assets.ts still imports it)
- Drop --disable-software-rasterizer from playwright.store-screenshots
  config to match fdb84c0f's stated intent
2026-05-08 22:30:59 +02:00
Johannes Millan
1d52843cd7 feat(video): Playwright-driven marketing reel pipeline
Adds an end-to-end pipeline for generating the marketing reel via
Playwright, including tight/full variants, drag ghost, integrations
layout, animated stats, brand-color logos, fade-to-black scene cuts,
and a handover CLAUDE.md for the reel pipeline.
2026-05-07 23:08:24 +02:00
Johannes Millan
7157ea62df feat(screenshots): polish app-store capture pipeline
Iterates the store-screenshot pipeline to ship-ready quality. Squashed
from a series of capture/build/UX changes that all live in
e2e/store-screenshots and form a coherent set.

Pipeline UX
- Print master capture path via Playwright globalTeardown.
- Declare sharp as devDep (was imported by build-store-assets but
  never listed) so a fresh install runs the build cleanly.
- Open dist/ folder when build finishes; opt out via
  SP_SCREENSHOTS_NO_OPEN=1.
- Emit dist/screenshots/_preview.html contact sheet for one-click QA
  across every per-store layout.

Capture content
- New slot 00 hero across desktop / mobile / tablet specs:
  showMarketingOverlay paints a gradient caption strip on top of the
  live app. Position is orientation-aware (bottom for landscape, top
  for portrait). Copy lives in marketing-copy.ts as a single source
  of truth.
- Desktop slot 05 captures the running focus timer instead of the
  duration picker (skip the rocket countdown via clock.runFor).
- Desktop slot 07 = plain dark project view (no wallpaper) instead
  of catppuccin; slot 08 = desktop planner.
- Mobile slots 02 and 04 use signal-based planner expansion (the
  component is gesture-only, so flip isExpanded via ng.getComponent).
- Side nav collapsed for desktop slots 01 (schedule day-panel) and
  04 (notes panel) so the right panel can breathe.
- Mobile hides the per-task play column via
  appFeatures.isTimeTrackingEnabled.
- Seed: drop Morning yoga (the only top-level done task on today),
  flip the remaining done subtask back to undone, trim the PR
  review tag chips down to the two EM tags that drive Eisenhower.

Viewports
- desktopMaster: 1280x800 CSS at 2x -> 2560x1600 (smaller MAS Retina
  size = more apparent zoom than the prior 1440x900 baseline).
- androidPhone: 412x915 CSS at 3x -> 1236x2745, matching modern
  flagships (Pixel 7/8, Galaxy S22+).
- Tablet rotations: 7" -> landscape, 10" -> portrait. android7Tablet
  moves from PHONE_VIEWPORTS to TABLET_VIEWPORTS.

Test infra
- Bump per-test timeout to 8 minutes for multi-locale single-session
  specs (12+ captures per locale overran the 180s default on slow
  viewports).
- Scope LOCALES to ['en'] for now until German strings catch up.
- ImportPage race tolerates slow imports without an encryption
  dialog: catch the 15s waitFor rejection so the 60s import-complete
  promise is the real ceiling. A cold dev server would otherwise
  abort here at 15s.
2026-05-07 18:25:25 +02:00
johannesjo
2fabe1bdc6 feat(screenshots): single-session capture, tablet spec, polished seed
Fold the iterative work from this session into one commit:

- Single-session capture across locales / themes / customTheme. Drives
  variant switches via NgRx dispatch through `__e2eTestHelpers.store`
  (re-enabled for non-prod/non-stage builds in src/main.ts), so one
  Chromium / Electron session covers every desktop and mobile permutation
  without relaunching the app or re-importing the seed. Replaces the
  per-(locale,theme) split specs with consolidated `desktop/all.spec.ts`
  and `mobile/all.spec.ts`.
- Dedicated tablet spec (`scenarios/tablet/all.spec.ts`) for the iPad 13"
  and Android 10" tablet viewports, which render SP's desktop layout (≥
  768 mobile-breakpoint) but on a narrower / taller canvas than
  desktopMaster. Added PHONE_VIEWPORTS / TABLET_VIEWPORTS in matrix.ts and
  routed each viewport class to the right spec via testMatch in the
  playwright config. Tablet scenes are panel-free and hover-free so they
  fit a 960/1032-wide canvas under emulated touch.
- Helpers for in-session theme / locale / customTheme flips
  (`applyTheme`, `applyLocale`, `applyCustomTheme`). screenshotMaster
  reads the live theme from `localStorage.DARK_MODE` and the live locale
  from `window.__spCurrentLocale` so captures land in the correct
  `<locale>/<theme>/` subdir.
- Suppress Material tooltips via injected CSS, park the cursor at (0, 0)
  before each capture, and gracefully fall back to `page.screenshot()`
  when OS-level capture fails (e.g. macOS Screen Recording permission
  missing). Switch the Electron launch to `NODE_ENV=PROD` so DevTools
  doesn't auto-open on dom-ready.
- Catppuccin scene now navigates to a project view
  (`desktop-07-project-catppuccin`) so the custom palette reads against
  the project primary, not a wallpaper. Seed builder no longer forces a
  bg image onto project themes — only tag themes get one, and the
  "Darken/lighten background image for better contrast" slider
  (`backgroundOverlayOpacity`) defaults to 80 (was 20). Override via
  `SP_SCREENSHOT_BG_OVERLAY_OPACITY` (0–99).
- Seed polish: soften EM_URGENT/EM_IMPORTANT colors, drop the '@' prefix
  from tag titles, remove tag-waiting, trim noisy urgent/important tags
  from most tasks, give the deep-work tag a hue distinct from work and
  open source, flip `isConfirmBeforeExitWithoutFinishDay` to false so
  Electron closes cleanly between runs, and enable the bg tint on the
  TODAY tag (was disabled by default).
- matrix.ts: rename `msstore` → `microsoft-store` output to avoid
  confusion with `macappstore`; wire flathub rule sourcing from the
  Electron pipeline; add `maxBytes` for Snap (2 MB JPEG re-encode).
- Add `npm run screenshots:electron` umbrella script that chains
  `screenshots:capture:electron && screenshots:build` so the macappstore
  / flathub deliverables land in `dist/screenshots/` in one go.
2026-05-07 01:06:22 +02:00
Johannes Millan
f98f80efe8 feat(screenshots): automated app-store screenshot pipeline
Playwright-driven pipeline that captures recognizable app states across all
target storefront viewports, locales, and themes from a single curated seed
dataset. Web Chromium pipeline feeds MS Store / Snap / Play / iOS / F-Droid
/ web; an Electron pipeline (with OS-level region capture for native window
chrome) feeds Mac App Store and is wired for Flathub.

Five grouped spec files capture multiple scenarios per (locale, theme,
customTheme) tuple in one session; transient UI state is reset between
scenes via page.reload() rather than relaunching contexts.

Outputs:
  .tmp/screenshots/_master/<viewport>/<locale>/<theme>/<scenario>/...
  .tmp/screenshots/_master_electron/...
  dist/screenshots/{macappstore,msstore,snap,web,ios,play,fdroid}/...

Run:
  npm run screenshots                  # web pipeline + per-store layout
  npm run screenshots:capture:electron # Mac App Store / Flathub source
2026-05-06 22:00:12 +02:00
Johannes Millan
faa45fd280 18.4.4 2026-05-02 20:20:10 +02:00
johannesjo
30d7e7d717 18.4.3 2026-05-02 00:03:46 +02:00
johannesjo
1b4d0c1710 18.4.2 2026-05-01 23:07:19 +02:00
Johannes Millan
ff0f6d2f82 18.4.1 2026-05-01 21:59:17 +02:00
Johannes Millan
4f15e66f9d 18.4.0 2026-05-01 20:58:26 +02:00
Johannes Millan
d7ca08552e build: downgrade eslint 2026-05-01 19:35:28 +02:00
Johannes Millan
d9f54efbb9 chore(deps): bump stylelint 16->17 + recommended-scss + formatter-github
Paired migration:
- stylelint 16.26 -> 17.9
- stylelint-config-recommended-scss 14.1 -> 17.0
- @csstools/stylelint-formatter-github 1.0 -> 2.0 (requires stylelint 17)

Fix the 26 violations from new deprecation rules:
- 'word-wrap' replaced with 'overflow-wrap' (autofix; word-wrap is a
  legacy alias in CSS3, only matters that stylelint now flags it)
- 'word-break: break-word' (deprecated keyword) replaced with
  'overflow-wrap: anywhere' (the spec-defined equivalent), with stale
  duplicate 'overflow-wrap' declarations consolidated
2026-05-01 19:35:27 +02:00
Johannes Millan
277cf1480e chore(deps): bump cross-env, eslint, jasmine-core, typia to next major
- cross-env 7→10 (CLI-compatible, Node 20+ engine — already met)
- eslint 9→10 + @eslint/js 9→10 (all active plugins support v10;
  fix local rule require-hydration-guard to use context.sourceCode
  instead of removed context.getSourceCode())
- jasmine-core 5→6 (forbidDuplicateNames default flipped; no duplicates
  in our suite — verified by running the full test:once)
- typia 11→12 (public API surface unchanged; validation specs pass)

Lint, build and the typia/auto-fix specs verified green. The 5 pre-existing
failures in immediate-upload.service.spec.ts are unrelated — also fail on
master with the same package.json that has been shipping.

Deferred from this round: stylelint 16→17 (paired migration with
@csstools/stylelint-formatter-github 1→2, separate work),
electron-dl 3→4 (forces full electron-main CJS→ESM migration),
marked 17→18 (ngx-markdown@21.2.0 peer-locks marked at ^17).
2026-05-01 19:35:27 +02:00
Johannes Millan
cbbec922b8 chore(deps): bump non-Angular deps to latest within current major
Patch/minor updates across tooling and runtime:
electron 41.2→41.4, electron-builder 26.7→26.8, prettier 3.7→3.8,
playwright 1.57→1.59, typescript-eslint 8.52→8.59, karma 6.4.2→6.4.4,
core-js 3.47→3.49, nanoid 5.1.6→5.1.11, dotenv 17.3→17.4, glob 13.0→13.0.6,
fs-extra 11.3→11.3.4, tslib 2.7→2.8, baseline-browser-mapping 2.9→2.10,
plus type defs and native binaries (@lmdb/*, @rollup/*).
Build, lint and a sample unit test all pass.
2026-05-01 19:35:27 +02:00
Johannes Millan
6a4253bd23 chore(deps): update Angular ecosystem to latest 21.x
Bumps Angular core/CLI/Material/CDK to 21.2.11/21.2.9, @ngrx/* to 21.1.0,
ngx-markdown to 21.2.0. All within current major; build and lint pass.
2026-05-01 19:35:27 +02:00
Johannes Millan
81560f97af 18.3.0 2026-04-25 22:42:18 +02:00