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.
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.
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.
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.
- 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.
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.
- 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
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.
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
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.
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.
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.
- 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).
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.
- 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).
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.
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.
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.
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
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.
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.
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
* 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.
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
* 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).
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>
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.
* 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>
* 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>
* 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>
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
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>
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
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.
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.