Commit graph

20780 commits

Author SHA1 Message Date
Johannes Millan
0f44abb72f fix(tasks): reliably convert subtasks dragged to the top-level list
Two issues prevented dragging a subtask out to the top-level list to
convert it into a main task:

- CDK only caches a sibling drop-list's geometry when its enterPredicate
  passes at drag start (_startReceiving). The pointer is always over the
  source subtask list then, so the top-level list was never cached and
  conversion silently failed until an unrelated parent drag warmed it.
  Open a one-microtask accept window at subtask drag start so CDK caches
  the top-level lists' geometry; the pointer guard resumes afterwards.

- An expanded neighbour's subtask list "caught" the drag in the dead-band
  just above the next parent (sibling order resolves subtask lists before
  the top-level list), silently re-parenting the subtask instead of
  converting it, and growing/sticking once entered. Treat only an actual
  subtask row as "inside" a foreign subtask list; its trailing padding now
  falls through to the top-level list for conversion. The source list
  still blocks anywhere, so in-list sorting is unaffected.
2026-06-01 21:52:22 +02:00
Johannes Millan
ed43940e09 fix(tasks): remove empty subtask drop-target for childless parents
The dashed empty sub-task drop zone that appeared under childless parent
tasks during a drag was unwanted UI. Remove it along with its supporting
machinery (the subTaskDropCandidate signal in ScheduleExternalDragService,
the pointerdown candidate-arming in TaskListComponent, the
isEmptySubTaskDropTargetMounted computed, and the related template/SCSS).

Consequence: a task can now be nested by dragging only onto a parent that
already has a subtask list. Dragging a subtask back out to a main list
(convertToMainTask) and nesting into an existing subtask list
(convertToSubTask) are unchanged.
2026-06-01 18:55:09 +02:00
Johannes Millan
96b4a2aa69 fix(tasks): harden convertToSubTask guards and dedupe tag cleanup
Address review findings on the drag-to-subtask feature (#7905):

- Keep the section and crud meta-reducer guards in lock-step via a shared
  canApplyConvertToSubTask(). Previously the section reducer stripped a task
  from its section even when the crud reducer rejected the convert (missing
  target parent or self-target), leaving the task top-level yet dropped from
  section ordering on a replayed/concurrent op.
- Reject nesting under a target that is itself a subtask. The UI renders only
  two levels, so deeper nesting would orphan the task and leave the
  grandparent's time aggregation stale.
- Tighten op-log payload validation to require string taskId/targetParentId.
- Dedupe the "remove task ids from all tags" logic shared by
  convertToSubTask/deleteTask/deleteTasks into removeTasksFromAllTags().
- Collapse the tri-state afterTaskId positioning in handleConvertToMainTask
  (undefined and null both prepend via moveItemAfterAnchor).
- Name the DragPointer type in DropListService.

Adds reducer specs for the rejected-target-parent cases.
2026-06-01 18:55:09 +02:00
kei
1c56d4e83a test: skip onboarding in migration fresh-start e2e 2026-06-01 18:51:35 +02:00
kei
53c4102443 fix(tasks): address drag conversion review 2026-06-01 18:08:44 +02:00
kei
bc758069ac fix(tasks): address subtask drag review feedback 2026-06-01 17:54:17 +02:00
kei
fadb1136a9 fix(tasks): preserve nested subtask sorting 2026-06-01 17:54:16 +02:00
kei
97f47eb742 fix(tasks): support dragging subtasks back to main list 2026-06-01 17:53:53 +02:00
kei
9d47ec8e22 fix(tasks): support childless subtask drop targets 2026-06-01 17:53:53 +02:00
kei
ed86a93783 feat(tasks): allow dragging tasks into subtask lists 2026-06-01 17:53:53 +02:00
Johannes Millan
2065a3ccaa refactor(ui): prune dead progress-bar labels and tighten collapsible mixin
Post-review cleanup of dead code adjacent to the previous commit:
- Reduce PROGRESS_BAR_LABEL_MAP to the only labels still reachable from a
  countUp() caller (POLL, /issue/); drop the now-unused SYNC/ASSETS/DBX_*/
  WEB_DAV_* entries and their GPB.* strings in en.json (regenerated
  t.const.ts). Unknown URLs already fall back to T.GPB.UNKNOWN.
- Move the collapsible-only :host.isInline rules out of the shared mixin
  into collapsible.component.scss so the mixin is a true common base and
  formly-collapsible no longer ships rules it can never trigger.
2026-06-01 17:47:43 +02:00
Johannes Millan
209db4881a refactor(ui): drop dead progress-bar module and dedupe collapsible styles
- Remove unreferenced GlobalProgressBarModule and the interceptor it
  uniquely registered (dead since the 2024-12-30 standalone migration;
  progress is now driven by explicit countUp/countDown in jira/issue
  services). Component + service are kept.
- Extract shared collapsible SCSS into _collapsible-shared.scss mixin,
  used by both collapsible and formly-collapsible; keep only per-component
  overrides. Fix invalid 'transition: all var(--transition-standard)'
  (double 'all' -> dropped) so the expand icon animates again.
- Drop dead commented-out block from formly-collapsible template.
2026-06-01 17:47:43 +02:00
Johannes Millan
d04ebb5401 refactor(config): reduce duplicated form and selector boilerplate
Add a typed kbField()/subSectionHeading() builder to the keyboard form
and a generic createConfigSectionSelector() factory in the global-config
reducer. Collapses ~48 repeated keyboard field blocks and 16 identical
`cfg?.x ?? DEFAULT_GLOBAL_CONFIG.x` selectors into one-liners; the
builder now type-checks keys against keyof KeyboardConfig.

Field key set and order are unchanged; selectClipboardImagesConfig,
selectIsFocusModeEnabled and selectTimelineWorkStartEndHours stay
hand-written. Drops the dead commented goToFocusMode duplicate only.

Part of #7922.
2026-06-01 17:42:01 +02:00
Johannes Millan
4ab7d2b465 test(markdown): strengthen markdown-parser specs from review
- assert convertToMarkdownNotes preserves absolute indentation (base-indented
  input) instead of a column-0 case that proved nothing
- add tab + no-space-bracket round-trip coverage for convertToMarkdownNotes
- add the at-limit (length 800000) boundary case to the max-length test
- use an explicit \uFEFF escape for the BOM test (a literal BOM is invisible
  and a formatter could silently strip it)
- document normalizeIndentation's in-place mutation contract
2026-06-01 17:38:57 +02:00
Johannes Millan
118f077318 test(markdown): cover convertToMarkdownNotes and shared input handling
Add direct coverage for the previously-untested convertToMarkdownNotes
export, exercise the shared parseLines/splitMarkdownLines setup (CRLF,
BOM, max-length), and add a tripwire documenting the walker divergence
a future shared top-level walker must preserve.
2026-06-01 17:38:57 +02:00
Johannes Millan
089a99769a refactor(core): extract shared setup in markdown task parsing
Fold the duplicated input validation, line split/parse, empty-result
guard and min-indent normalization shared by parseMarkdownTasks,
parseMarkdownTasksWithStructure and convertToMarkdownNotes into
parseLines() + normalizeIndentation(). Extract the identical per-line
checkbox formatting into formatAsCheckboxLine(). The two top-level
walkers keep their distinct nested-item behavior untouched.

Refs #7918
2026-06-01 17:38:57 +02:00
Johannes Millan
52871f752e refactor: remove dead code (#7911)
Remove verified-dead code surfaced by the #7911 simplification sweep
(all confirmed to have zero production references):

- util/create-sha-1-hash.ts (legacy local-file sync; had a broken
  import + leftover console.time)
- util/omit.ts, util/watch-object.ts, util/numeric-converter.ts
- selectAllTasksDueAndOverdue, selectTasksWorkedOnOrDoneFlat selectors
  (plus their specs and now-unused imports)
- getTagOrUndefined helper
- stale commented-out blocks in the task components
- dangling watch-object.ts reference in remove-unused-log-imports.ts

unschedule() was intentionally kept (still called internally), despite
being listed as dead.
2026-06-01 17:38:57 +02:00
Johannes Millan
14a085a2ee refactor(tasks): close detail panel explicitly in hideDetailPanel
hideDetailPanel() previously called setSelectedId(this.task().id),
identical to showDetailPanel(). It only closed the panel by relying on
the setSelectedTask reducer toggle (id === selectedTaskId -> null) plus
the isSelected() guard in handleArrowLeft. Behavior is unchanged, but
setSelectedId(null) makes the intent explicit and drops the toggle
reliance.
2026-06-01 17:38:57 +02:00
Johannes Millan
4be7b5358f refactor: remove dead code and deduplicate platform/op-log helpers
- capacitor-platform: inject IS_ANDROID_WEB_VIEW_TOKEN instead of an
  inline window.SUPAndroid check
- task.selectors: extract isCalendarIssueTask helper (was duplicated)
- sync-import-filter: use isFullStateOpType helper for op-type checks
- take-a-break: drop unreachable BREAK branch in reduce (guarded above)
- plugin-api: remove unused header/menu/shortcut/side-panel tracking arrays
- task-list / task-hover-controls: remove unused drop() arg and inputs
- is-today: remove unused isYesterday
- migrate-to-droid-log: collapse repeated import checks into one regex
2026-06-01 17:38:57 +02:00
Johannes Millan
0396455841 fix(caldav): match getByIds$ issues by id value not array index
The filter used `task.id in ids`, which tests whether task.id is a
key/index of the ids array rather than a member — so it never matched
real issue ids. Use a Set membership check instead.
2026-06-01 17:38:57 +02:00
Johannes Millan
302d5fea12 refactor: address multi-review feedback
- Relocate the memory-efficiency rationale and no-early-exit NOTE from the
  now one-line getLatestFullStateOp wrapper onto getLatestFullStateOpEntry,
  which actually owns the cursor loop.
- getPointerPosition reuses the existing Safari-aware isTouchEvent guard
  instead of re-inlining the 'touches' in event check (free type-narrowing).
2026-06-01 16:56:59 +02:00
Johannes Millan
313625ac11 refactor(tasks): drop unused isIgnoreShortSyntax param from handleUpdateTask
The meta-reducer never read the param (short-syntax handling lives in
short-syntax.effects.ts). The action field is untouched.
2026-06-01 16:56:59 +02:00
Johannes Millan
418a66b756 refactor(util): drop redundant Array.isArray branch in distinctUntilChangedObject
Arrays satisfy isObject ([] === Object([])), so the Array.isArray clause
could only be true when the isObject clause already was.
2026-06-01 16:56:59 +02:00
Johannes Millan
c9bf268958 fix(issue): show provider name in Redmine not-configured snack
The ERR_NOT_CONFIGURED string interpolates {{issueProviderName}}, but
Redmine passed the value under the key ISSUE_PROVIDER_HUMANIZED, so the
provider name never rendered. Matches gitea/open-project.
2026-06-01 16:56:59 +02:00
Johannes Millan
47db293957 refactor(ui): extract shared getPointerPosition util
magic-side-nav and schedule-day-panel each defined a byte-identical
private _getPointerPosition. Move it to a pure util and reuse.
2026-06-01 16:56:59 +02:00
Johannes Millan
02984e14dc refactor(op-log): dedupe full-state-op and entity-id helpers
- getLatestFullStateOp now delegates to getLatestFullStateOpEntry (same
  reverse-cursor scan and UUIDv7 max-selection; returns the .op).
- clearFullStateOps now delegates to clearFullStateOpsExcept([]) (empty
  exclude set = delete-all; same scan, delete tx, cache invalidation).
- Extract getOpEntityIds() for the entityIds/entityId normalization that
  was duplicated verbatim in the store, conflict-resolution, and
  vector-clock services.

Behavior-preserving; existing op-log specs pass (store 170, conflict 132,
vector-clock 27).
2026-06-01 16:56:59 +02:00
Johannes Millan
4ec540d40f refactor(tasks): collapse duplicate moveTask component methods
The four moveTask* methods shared the same task lookup, service call
shape, and double-setTimeout refocus quirk. Extract _moveAndRefocus and
pass the service move call per direction.
2026-06-01 16:56:59 +02:00
Johannes Millan
cb800a27df refactor(tasks): collapse duplicate moveSubTask reducer cases
The four moveSubTask* handlers were identical except the arrayMove*
function. Extract a reorderSubTask(state, id, parentId, moveFn) helper
and pass the move function per case.
2026-06-01 16:56:59 +02:00
Johannes Millan
64626c4102 refactor(util): remove unused uniqueByProp helper
uniqueByProp had zero usages and duplicated dedupeByKey.
2026-06-01 16:56:59 +02:00
Johannes Millan
361d1c3ac5 refactor(util): consolidate array-equality helpers into fastArrayCompare
Replace the duplicate arrayEquals and distinctUntilChangedSimpleArray
helpers with the existing generic fastArrayCompare. arrayEquals also
lacked a length check (latent equality bug); fastArrayCompare includes
one, so callers comparing sorted key-sets are now strictly correct.
2026-06-01 16:56:59 +02:00
Johannes Millan
ef5b2fe704 docs(readme): remove TestMu AI sponsor 2026-06-01 16:36:35 +02:00
Johannes Millan
87212472fa test(e2e): scope supersync backup-revert DB restore to test user
The backup-revert test restored via a global DROP SCHEMA public CASCADE + full pg_dump on the shared test Postgres. Under Playwright fullyParallel (3 workers/shard) this reverted concurrent @supersync tests' in-flight data mid-run, flaking supersync-snapshot-vector-clock (server latestSeq regressed, e.g. 4 to 1).

Replace with a per-user snapshot/restore of operations, user_sync_state and sync_devices via a COPY round-trip in one transaction, preserving exact last_seq so the SYNC_IMPORT_EXISTS path still fires. Other users' rows are never touched. Also correct the misleading #7810 comment in the snapshot test.

Verified green: both files pass across --repeat-each=10 with workers=3 (the parallel race reproduced, now clean).

Refs #7810
2026-06-01 16:33:18 +02:00
Johannes Millan
1eddb648e2 fix(onboarding): hide side-nav during first-run onboarding
The first-run onboarding preset screen is a full-window fixed overlay
(position: fixed; inset: 0). With a translucent theme (liquid-glass sets
--bg: transparent) the backdrop is see-through, so the side-nav shows
through it and its content overlaps the nav on narrow windows (#7885).

The onboarding is meant to be a focused takeover, so hide the nav (and
its mobile bottom-nav counterpart) while it is shown. This keeps the
takeover consistent across themes and removes the overlap by removing
the overlapped element.
2026-06-01 16:33:18 +02:00
Johannes Millan
3d1e173f67 fix(android): drop action bar and inset system bars on WebView block screen
The WebView block / init-failure screen was the only activity using a theme
with an action bar (AppTheme / Theme.AppCompat.Light.DarkActionBar), so the app
title bar overlaid the message. Its content also drew under the status bar on
edge-to-edge devices (enforced and not opt-out-able at targetSdk 36), clipping
the title's first line with no way to scroll it into view.

Switch the activity to the existing AppTheme.NoActionBar and inset the scroll
container by the system bars + display cutout so the message is fully visible.

Reported in issue #7840.
2026-06-01 16:33:18 +02:00
Johannes Millan
cbbf193d3d feat(project): toggle archive action to restore for archived projects
The project three-dots menu always showed "Archive project", even for an
already-archived project opened as the active context from the Archived
Projects list. Show "Restore project" instead when the project is archived,
wired to the existing unarchive flow.

Closes #7884
2026-06-01 16:33:18 +02:00
Johannes Millan
90288b885e
fix(android): recover focus/pomodoro timer after app swipe (#7855) (#7866)
* fix(android): recover focus/pomodoro timer after app swipe (#7855)

After #7818 kept the foreground services alive across task removal, the
focus-mode timer still reset and its notification closed on reopen, while
time tracking survived. Two causes:

1. On cold start, syncFocusModeToNotification$ computed wasFocusModeActive
   from the startWith(null) seed as (undefined \!== null) === true, so the
   first idle emission fired stopFocusModeService() and tore down the
   surviving native notification.
2. Focus mode had no native read-back/recovery (unlike time tracking), so
   the idle store never re-adopted the still-running native countdown.

Mirror the time-tracking recovery design:
- FocusModeForegroundService: mirror live timer state into the companion
  object + liveRemainingMs(); cleared on stop.
- JavaScriptInterface.getFocusModeElapsed() returns JSON or "null"
  (task title intentionally omitted — no user content crosses the bridge).
- restoreFocusSessionFromNative action + reducer rebuild the timer
  (elapsed = duration - remaining, startedAt = now - elapsed; Flowtime =
  duration 0) and derive mode from duration so a restored Flowtime session
  is not auto-completed on the next tick.
- recoverFocusSession$ recovers on cold start + resume, gated on an idle
  store and settled hydration; the wasFocusModeActive seed bug is fixed.

* fix(android): trigger focus recovery on resume edge only, not store changes

recoverFocusSession$ put selectTimer in its combineLatest trigger list, so
every idle-store emission re-read native state. Ending a session
(cancel/complete) emits an idle store while the native stop
(stopFocusModeService -> stopService -> onDestroy) is still in flight, so
getFocusModeElapsed() saw isRunning === true and re-adopted the session that
just ended -- resurrecting a cancelled session or double-logging a completed
one.

Sample selectTimer via withLatestFrom instead of using it as a trigger, so only
a genuine resume/cold-start edge can trigger recovery.
2026-06-01 16:15:56 +02:00
Johannes Millan
8f1bb8f7c6
fix(config): custom-title-bar default + drop no-op "None" project (#7907)
Bug 1: the "Use custom title bar" checkbox showed unchecked on a fresh
install even though the window enabled the custom title bar by default
(non-GNOME). Seed the Misc-settings checkbox with a display-only formly
defaultValue so it reflects the actual window state. It is deliberately
NOT added to DEFAULT_GLOBAL_CONFIG: a persisted default would be pushed
to Electron on every launch and override a legacy isUseObsidianStyleHeader
choice. Accepted residual: saving any other Misc setting emits the whole
model and persists the seeded value too, so a pre-2025-12 non-GNOME user
who had disabled the old obsidian-style header may then see the custom
title bar re-enabled (reversible via the checkbox).

Bug 2: the tasks "Default project" select offered a "None" option that
was a no-op -- an unset default still routed new tasks to the Inbox.
Hide the option for this field via a new opt-in hideNoneOption prop,
default the config to the Inbox, normalize legacy null/empty values to
the Inbox on load, and reset to the Inbox (not null) when the chosen
default project is deleted.

Also type the shared select-project component's custom props
(nullLabel, hideNoneOption) to match the SliderProps pattern.

Refs #7891
2026-06-01 16:15:32 +02:00
Johannes Millan
4d212f122e
fix(focus-mode): re-sync timer on Android resume (#7856) (#7867)
* fix(focus-mode): re-sync timer on Android resume (#7856)

The in-app Focus/Pomodoro countdown is driven by an RxJS interval(1000)
that Android/Chromium freezes for a backgrounded WebView, so no tick
fires while away and the in-app display drifts from the still-accurate
native notification (by ~the time spent in the background). Time tracking
avoids this by re-syncing from native on androidInterface.onResume$
(syncOnResume$); focus mode had no equivalent.

Fire a tick() on app resume so the wall-clock-based reducer
(elapsed = Date.now() - startedAt) snaps the countdown back to the truth.
The tick reducer no-ops for idle/paused timers, so the effect dispatches
unconditionally — mirroring the sibling handleFocusResume$ pattern.

Stream logic is extracted into the exported createFocusResumeTick$ factory
so the IS_ANDROID_WEB_VIEW-gated effect can be unit-tested under Karma.
focus-mode.bug-7856.spec characterises the drift mechanism and proves the
reducer ignores ticks on paused/idle timers (makes the unconditional
dispatch safe).

* refactor(focus-mode): extract notification-change check, log resume re-sync (#7856)

Builds confidence around the resume re-sync fix:

- Log on Android onResume$ so logcat shows the full reconciliation chain:
  "App resumed, re-syncing focus timer" -> tick -> "Updating focus mode
  service { remaining: <corrected> }".
- Extract the private _hasStateChanged into a pure, exported
  hasFocusNotificationStateChanged (dropping two unused params), so the
  notification-reconciliation logic is unit-testable.

Tests prove the loop closes for BOTH displays: a resume tick's large elapsed
jump crosses the 5s throttle, so the corrected remaining is pushed to native —
fixing the notification side too, not just the in-app countdown.

* refactor(focus-mode): extract native timer-complete guard for testability (#7856)

The IS_ANDROID_WEB_VIEW-gated handleNativeTimerComplete$ effect cannot be
instantiated under Karma, so its inline filter had no test. Extract the guard
into the pure, exported shouldHandleNativeTimerComplete (mirroring the existing
createFocusResumeTick$ / hasFocusNotificationStateChanged extractions) and unit
test it. The work-session arm is what makes a native completion a no-op once a
resume tick already finished the session, so the two never double-complete.

* test(focus-mode): cover completion-while-backgrounded race (#7856)

When a session runs past its duration while backgrounded, both the resume
tick() and the native ACTION_TIMER_COMPLETE event are in play on resume (the
broadcast receiver lives onCreate..onDestroy, so the native completion is not
dropped). Walk the real reducer through both orderings and pin the JS-side
invariant: exactly one clean completion, no double-dispatch and no dropped
completion. The two effect guards are tied to their real code — the exported
shouldHandleNativeTimerComplete and the real detectSessionCompletion$ (idle
case added here) — rather than re-encoded. Also documents the benign duration
nondeterminism (over-run elapsed vs native-capped duration).
2026-06-01 15:42:37 +02:00
JongoDB
91a407346d
docs(caldav): mark VEVENT two-way as shipped via the bundled plugin (#7879)
The CalDAV VEVENT expansion design doc still says 'Status: Planned', but two-way
VEVENT sync + task->event time-blocking shipped via the bundled CalDAV Events
plugin (packages/plugin-dev/caldav-calendar-provider), not by extending the core
CalDAV provider as the doc proposed. The core provider remains VTODO-only.
Update the status banner + add an as-built note; retain the historical design.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:31:45 +02:00
Johannes Millan
1cc8a8eb2d
test(e2e): de-flake three scheduled-run E2E failures (#7862)
Fixes the flaky failures observed in the scheduled master E2E run
(actions/runs/26672979319), all single-attempt (retries: 0 by design):

- sections DnD ("drag source/target has no bounding box"): stableBoundingBox
  polled until a box existed then re-read it, reopening the TOCTOU race the
  poll closes. Capture the validated box inside the poll and return it.

- focus auto-start indicator: the running-label assertion used a hardcoded
  5s timeout, tighter than the suite's 20s default (set for slow rendering).
  The indicator is gated behind an async effect chain, so inherit the
  default timeout instead.

- wrong-password "overwrite remote" confirm button stayed disabled: the old
  helper committed input values, then separately asserted the button enabled,
  with no recovery when form validity lagged. Fold the re-fill and the
  toBeEnabled check into one toPass so a still-disabled button re-triggers a
  re-fill.
2026-06-01 14:31:26 +02:00
afonso-resendes
916fe188c8
Fix #5461: Task dates with delayed start-of-day (#7503)
* Fix #5461: Task dates with delayed start-of-day

Fixes incorrect behavior after midnight when start-of-next-day is set

(e.g. 3 AM). Time between midnight and that hour maps to

the correct logical day.

* test(add-task-bar): mock getLogicalTodayDate for #5461

* test(date): address PR review for #5461

Treat out-of-range start-of-next-day hours as 0.

Add logical-tomorrow tests with non-zero offset.

* test: address optional PR review nits for #5461

* test(add-task-bar): drop redundant mocks from logical-today display test

Mirror the simplification already applied to the logical-tomorrow test:
dateDisplay() only reads getLogicalTodayDate(), so the todayStr /
getStartOfNextDayDiffMs / Date.now stubs were circular and would let the
test pass even if dateDisplay stopped honoring the offset.

* test(add-task-bar): cover offset-aware today for repeat-preset tasks

The repeat-preset dueDay and repeat-config startDate paths (added for #5461)
had no behavioral coverage — reverting either to getDbDateStr() left all
specs green. Add two tests that mock todayStr() to a fixed past date and
assert addTask() forwards it to taskData.dueDay and the repeat-cfg startDate,
so a regression to calendar-midnight is caught.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-01 14:20:15 +02:00
RoyS
90e13b183a
fix(header): keep actions visible with long titles (#7898)
* fix(header): keep action buttons visible with long titles

* test(header): replace brittle styles-string check with DOM layout test

Replaces the unit test that introspected Angular's private \u0275cmp.styleswith a behavioral test that renders PageTitleComponent inside a constrained-width flex row, sets a long active work-context title, and asserts:

- the title element actually overflows its rendered box (ellipsis active)
- the trailing actions remain fully contained within the row

This exercises the real CSS contract introduced by the fix (lex: 1 1 auto/min-width: 0 on .page-title, lex: 0 0 auto on .page-title-actions) instead of grepping the source string, and survives Angular internals changes.

* refactor(header): simplify long-title fix to the minimal flex change

The title already ellipsizes via its existing `overflow: hidden` (which
makes the flex auto-minimum size 0), so the added `min-width: 0` was
redundant and `flex-grow: 1` only enlarged the title's click/hover area
without affecting the overflow fix. Keep the load-bearing
`flex: 0 0 auto` rules on `.action-nav-right` / `.page-title-actions`
and document why. Re #7477.

* test(header): make long-title regression test actually guard the bug

The previous page-title DOM test passed with or without the fix
(verified by reverting it): it leaned on the pre-existing
`overflow: hidden` and hardcoded the trailing nav as non-shrink, so it
never exercised the real `.action-nav-right { flex: 0 0 auto }` change.

Replace it with a main-header spec that loads the real stylesheet via
`styleUrls` and reproduces the failing flex structure (constrained row,
shrinkable title, `flex-shrink: 0` counter buttons). It fails when the
fix is reverted. Re #7477.

---------

Co-authored-by: RoyS <roy.serbi@users.noreply.github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-01 13:59:23 +02:00
Johannes Millan
6bb030802e
fix(sync): guard against caching empty state over good data (#7892) (#7899)
* fix(sync): guard against caching empty state over good data (#7892)

A local data-loss report (blank app after an overnight gap, no sync, on
Android) surfaced a robustness hole: a transient hydration/IndexedDB
glitch could leave NgRx in its initial empty state, which would then be
written back over the good state cache and, on the next compaction, prune
the very ops needed to recover — turning a recoverable hiccup into
permanent loss.

- Add `hasMeaningfulStateData()` as the single source of truth for "does
  this state contain user data?" (task / non-INBOX project / non-system
  tag / note). Reused by `SyncLocalStateService.hasMeaningfulStoreData()`
  which previously duplicated the predicate.
- Snapshot save (`saveCurrentStateAsSnapshot`) and compaction
  (`_doCompact`) now skip when the live state has no meaningful data,
  refusing to overwrite the cache (and, in compaction, refusing to prune
  ops) against empty state. Skipping is always safe: the op-log is the
  source of truth and replay reconstructs the true state, including
  legitimate full wipes.
- Hydrator: when the state cache is invalid/corrupt but the op-log is
  intact (lastSeq > 0), replay the log from the start instead of dropping
  straight to recovery-to-empty.

Adds unit tests for the new predicate; existing snapshot/compaction/
hydrator/sync-local-state specs still pass.

* test(sync): cover empty-state guard in snapshot/compaction specs (#7892)

Adjust the existing snapshot and compaction specs for the new
empty-overwrite guard, and fix the new predicate spec's fixture.

- has-meaningful-state-data spec: use the real SYSTEM_TAG_IDS values
  (TODAY / EM_URGENT / EM_IMPORTANT / KANBAN_IN_PROGRESS) so the
  "default/initial state" and "system tags only" cases are accurate.
- snapshot spec: default the state mock to meaningful data so the
  clock-pruning / compactedAt / entity-key tests still exercise the save;
  add a dedicated test that an empty state is skipped.
- compaction spec: default mockState to meaningful data; convert the
  former empty-state tests to assert the guard skips saveStateCache and
  op deletion; give the entity-key-extraction states a live task so they
  pass the guard.

All affected unit specs pass (152).

* docs(sync): clarify compaction empty-state guard trade-off (#7892)

Correct the `_doCompact` guard comment: the previous wording ("the next
compaction with real state handles it") overstated safety. A store that is
genuinely empty-but-active never gets its old synced ops pruned while it
stays empty, so the op-log can grow. Document this as an accepted trade-off
rather than implying it always self-heals. No behaviour change.

https://claude.ai/code/session_01Lwt1t6AFwJGX6gNsuGoZXN

* test(sync): cover hydrator corrupt-snapshot op-log replay (#7892)

The headline #7892 fix — the hydrator discarding a corrupt/invalid
state-cache and replaying the op-log when lastSeq > 0, instead of dropping
to recovery-to-empty — had no test, and the two pre-existing
"invalid snapshot handling" tests asserted the old unconditional-recovery
behaviour without pinning lastSeq. Under Karma's randomized order a bled-in
lastSeq > 0 made one of them take the new replay branch and fail.

- Pin lastSeq = 0 in the two recovery tests so they deterministically
  exercise the (now conditional) recovery-to-empty branch, and rename them
  to state that precondition.
- Add a test proving an invalid snapshot with lastSeq > 0 discards the
  snapshot, does NOT call attemptRecovery, and replays the full op-log via
  bulkApplyHydrationOperations.
- Add a test for the lastSeq === 0 recovery branch.

Hydrator spec: 67/67, stable across repeated randomized runs.

https://claude.ai/code/session_01Lwt1t6AFwJGX6gNsuGoZXN

* test(sync): fix perf integration spec for empty-state compaction guard (#7892)

`performance.integration.spec.ts > should compact operations efficiently`
stubbed `getAllSyncModelDataFromStore` (which compaction does not call) and
left the shared `getStateSnapshot` default returning an empty store. With
the new empty-state guard, compaction now correctly skips an empty state,
so `loadStateCache()` returned null and the test failed. Give the test a
task-bearing `getStateSnapshot` so compaction runs. No production change.

All six compaction/snapshot integration specs pass (78).

https://claude.ai/code/session_01Lwt1t6AFwJGX6gNsuGoZXN

* test(sync): cover empty-state compaction guard against real IndexedDB (#7892)

- add A/B integration test proving an empty live state cannot overwrite a good on-disk state cache or prune ops (real IndexedDB, not spies)
- clarify in hasMeaningfulStateData JSDoc that the narrow scope is deliberate and only ever used in the safe (skip-not-overwrite) direction

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 13:51:30 +02:00
Johannes Millan
726d877d17
fix(sync): point manual supersync image push at super-productivity org (#7871)
build-and-push.sh defaulted the image namespace to the deprecated ghcr.io/johannesjo/supersync — the source of the stale 7-day-token :latest image in #7865. Default to the super-productivity org instead, matching the CI workflow, docker-compose.yml and the Helm chart.

Since you cannot docker login as an org, split the image namespace (GHCR_NAMESPACE, defaulting to the org) from the GHCR login account (GHCR_USER), and fail fast if a push is attempted with no GHCR_USER set.

Refs #7865
2026-06-01 13:32:06 +02:00
JongoDB
0149ed883e
feat(caldav): make VTODO poll interval configurable (#7881)
The core CalDAV VTODO provider polled on a hard-coded 10-min CALDAV_POLL_INTERVAL,
sluggish for an actively-shared list (the bundled CalDAV Events plugin polls 60s).
Expose a per-provider 'Poll interval (minutes)', mirroring the existing
pollIntervalMinutes pattern in NextcloudDeckCommonInterfacesService:
- CaldavCfg.pollIntervalMinutes?: number (optional; existing cfgs fall back to the const)
- pollInterval is now a getter deriving from the cached cfg via a _getCfgOnce$ override
- advanced-config form field (number, min 1) + en.json + t.const.ts
- unit tests for the getter (fallback + derived)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:19:55 +02:00
Johannes Millan
ab5a2ffce9
fix(i18n): boot with bundled English when translation fetch fails offline (#7854) (#7875)
The app loads UI translations at runtime via ngx-translate's
TranslateHttpLoader (GET ./assets/i18n/<lang>.json). When that request
fails with a status-0 network error — e.g. opened offline via Safari's
Reading List, which serves the cached HTML but bypasses the service
worker that would otherwise serve the cached en.json — the stock loader
has no fallback. The rejection reaches GlobalErrorHandler, which treats
it as critical and renders a crash card instead of letting the app boot.

Wrap TranslateHttpLoader in TranslateHttpLoaderWithFallback: on a
status-0 failure, fall back to the English translations bundled at build
time so the app boots with readable text (self-healing once a cached or
online copy loads). Any other failure (404, JSON parse error = real
deploy problem) is rethrown so it stays visible.

Place it beside NetworkRetryInterceptorService in core/http, which
already handles the same status-0 transient-network class.

- unit: success / status-0 fallback (en + non-en) / 404 rethrow
- e2e: aborts the i18n fetch, asserts no crash card and that the
  side-nav renders real English (not raw MH.* keys)
- prod build verified: initial bundle 4.95 MB raw, under the 5.5 MB budget
2026-06-01 13:15:33 +02:00
Johannes Millan
1118e04ddf
fix(sync): preserve partial LWW relationships (#7876) 2026-06-01 13:13:09 +02:00
rubenmuqui
98e492eeca
Update Spanish translation for 'RESUME' key (#7877) 2026-06-01 13:04:46 +02:00
Johannes Millan
380b199152
test(sections): de-flake intra-section drag reorder e2e (#7861)
The "reorders tasks within a section via drag and drop" test
intermittently failed with "drag source/target has no bounding box"
(e.g. CI run 26681989260). Each CDK drop re-creates the moved task as a
fresh @for :enter, replaying the expandInOnly animation (height: 0 -> *),
so its layout box collapses to zero height mid-gesture and boundingBox()
returns null.

Disable animations for this test via the app's own isDisableAnimations
config (toggles @HostBinding('@.disabled')), which removes the height:0
window. Verified 5/5 green locally.

Scoped to the reorder test only; the cross-list "drops a task into a
section" test keeps animations on, since disabling them makes the empty
drop target reflow instantly and miss.
2026-06-01 13:04:07 +02:00
Johannes Millan
834bfd02f2
fix(task-repeat): allow selecting today as start date across timezones (#7768) (#7860)
The startDate picker's min was clamped to today (#7799), but
DatePickerInputComponent.toDate parsed string min/max via new Date(str)
(UTC midnight) while the MatDatepicker emits selections at local
midnight. In positive-offset timezones a selection equal to min (today)
compared as before-min and was silently nulled, so the field became
required-invalid — 'can't set start date to today'.

Parse string bounds with dateStrToUtcDate (local midnight), matching
writeValue and the picker. Add min/max same-day boundary regression
tests covering both offset directions via the dual-TZ suite.
2026-06-01 13:03:35 +02:00