Multi-review (W3) flagged that `@xmldom/xmldom` was a runtime
dependency only because the package's vitest env (Node) lacks the
DOM `DOMParser`. Browsers, Electron, and Capacitor WebViews all
provide their own `DOMParser` — so the dep was shipping into the
host's transitive bundle just to support the package's unit tests.
Move it to `devDependencies` and:
- Use `globalThis.DOMParser` at runtime in `webdav-xml-parser.ts`.
Added a minimal `declare const DOMParser: { new(): {
parseFromString(text, mimeType): XmlNodeLike } }` so the package's
strict TS still type-checks without DOM lib pulling in via
xmldom's `/// <reference lib="dom" />`.
- Polyfill `globalThis.DOMParser` from `@xmldom/xmldom` in a new
`tests/setup-dom-parser.ts` vitest setup file. Vitest config adds
it to `setupFiles`.
- Drop the explicit `import { DOMParser } from '@xmldom/xmldom'` from
the parser source.
Side effect: the package's `tsconfig.json` now sets `lib: ["ES2022",
"DOM"]`. Previously the `/// <reference lib="dom" />` directive
inside `@xmldom/xmldom`'s shipped `index.d.ts` was transparently
pulling DOM lib in for our TS DTS compilation (covering `Response`,
`RequestInit`, `URL`, `URLSearchParams`, `btoa`, etc.). Removing the
xmldom import broke that chain — the package code uses those DOM
globals throughout, so adding `DOM` to `lib` is the explicit fix.
Bundle impact
- Package's own ESM bundle: 73.23 KB (was 70.93 KB; +2.3 KB from
PR 6b's test-connection helper, not xmldom — xmldom was already
marked external by tsup since it was a dep, so the package bundle
never carried it).
- Host bundle savings: `@xmldom/xmldom` no longer transitively
installed for consumers — roughly 50 KB pre-gzip / ~15 KB gzip
recovered from app bundles.
Verification
- npm run sync-providers:test: 164/164 (setup file polyfills
DOMParser correctly; vitest reports 274 ms setup time).
- npm run sync-providers:build: ESM 73.23 KB / CJS 75.77 KB / DTS
37.13 KB. Confirmed `grep -c xmldom dist/index.mjs == 0`.
- All modified files pass `npm run checkFile`.
Lift WebdavBaseProvider, Webdav, NextcloudProvider, WebdavApi,
WebDavHttpAdapter, and WebdavXmlParser into @sp/sync-providers behind
the existing port surface — no new ports introduced this slice (per
multi-review consensus).
Architecture
- App side: thin createWebdavProvider(extraPath?: string) /
createNextcloudProvider(extraPath?: string) factories compose
NativeHttpExecutor / WebFetchFactory / ProviderPlatformInfo /
SyncCredentialStore deps internally, matching the Dropbox slice
precedent. sync-providers.factory.ts updated to call the factories.
- Package side: WebdavBaseProvider is generic on a WebdavProviderId
union (typeof PROVIDER_ID_WEBDAV | typeof PROVIDER_ID_NEXTCLOUD),
eliminating the four `as unknown as` casts the original code used
to share its base class across WebDAV and Nextcloud cfgs.
- Native HTTP path stays correct: the app injects an APP_WEBDAV_NATIVE_HTTP
adapter (in capacitor-webdav-http/app-webdav-native-http.ts) that
wires Capacitor's WebDavHttp plugin into the existing
NativeHttpExecutor port. The package's WebDavHttpAdapter selects the
native path via platformInfo.isNativePlatform; otherwise it goes
through WebFetchFactory. The inline registerPlugin('WebDavHttp')
call in the previous adapter is gone — the canonical registration
in capacitor-webdav-http/index.ts (with the web fallback) is the
only one now.
Hashing
- md5HashSync (spark-md5) replaced with hash-wasm's async md5 (already
a package dep used by PKCE). _computeContentHash on WebdavApi is
now async; ripples through download / upload / verify paths.
- Added @xmldom/xmldom as a package dep so the parser can run under
vitest's Node test env. The parser uses getElementsByTagNameNS('*',
name) so it works portably across browser DOMParser and xmldom
(xmldom does not implement querySelector).
Privacy sweep
- urlPathOnly applied at every URL-bearing error-construction and log
site in webdav-http-adapter (incl. PotentialCorsError, the new
HttpNotOkAPIError synthetic 500, RemoteFileNotFoundAPIError).
- errorMeta(e, extra) replaces every raw `SyncLog.error(..., e)` site
across webdav-api and webdav-http-adapter — ten+ sites converted to
structured `SyncLogMeta` (errorName / errorCode / safe primitives).
- _buildFullPath now throws InvalidDataSPError with a generic
"contains '..' or '//'" message instead of generic Error echoing
the user-supplied path.
- WebdavXmlParser.validateResponseContent's log no longer carries
`responseSnippet: content.substring(0, 200)` — only `contentLength`
and operation name go through the logger.
- testConnection retains its user-facing fullUrl + e.message (the
user is testing their own server config — this is intentional UX,
not a log), but routes the same error through `errorMeta` for the
separate structured log line.
- CORS heuristic at webdav-http-adapter.ts:180-219 (40 lines) collapsed
to a 3-line `TypeError && message.includes('cors')` check. Closes
the privacy leak (Firefox's NetworkError embeds the request URL)
and the prior false-positive where plain offline/DNS errors fired
PotentialCorsError.
Spec migration
- Jasmine specs converted to Vitest:
- webdav-xml-parser.spec.ts (15 tests, was 40)
- webdav-http-adapter.spec.ts (11 tests, was 18)
- webdav-api.spec.ts (18 tests, was 44)
- webdav-base-provider.spec.ts (9 tests, was 26)
- The TestableWebDavHttpAdapter subclass-override pattern is deleted;
tests inject the WebDavHttpAdapterDeps (platformInfo, webFetch,
nativeHttp, logger) directly. Native-routed tests un-skip cleanly.
- The package-level __mocks__/@capacitor/core.ts harness is deleted
(no longer needed — the package never imports @capacitor/core).
Dialog-sync-cfg
- src/app/imex/sync/dialog-sync-cfg now imports WebdavApi +
WebDavHttpAdapter from @sp/sync-providers and constructs them with
app-supplied deps for the "Test connection" UX. The user-facing
success/error snackbar uses result.fullUrl + result.error
unchanged.
Verification
- npm run sync-providers:test: 157/157 (was 103; +54 webdav specs).
- npm run sync-providers:build: ESM 70.93 KB / CJS 73.78 KB / DTS
34.40 KB (was 40/43/25, ~30 KB growth from WebDAV + Nextcloud +
@xmldom/xmldom).
- npm run lint: clean.
- npm run test:file file-based-sync-adapter.service.spec.ts: 58/58.
- npm run test:file sync-wrapper.service.spec.ts: 107/107.
Slice scope per multi-review consensus
- Open Q1: dropped the proposed WebDavNativeHttpExecutor port —
reused NativeHttpExecutor with options. Open Q2: hash-wasm async.
Open Q3: Nextcloud generic widening to union. Open Q4: inline
registerPlugin dropped. Open Q5: CORS heuristic tightened in-slice.
Open Q6: no-retry behavior preserved. Open Q7: TestableWebDavHttpAdapter
deleted. Open Q8: webdav-api.spec kept conceptually monolithic
(the package-side rewrite is smaller, but no second-file split).
- Documented gemini-dissent decisions in the slice design doc.
Defers (per consensus, not in this slice):
- local-file-sync-base.ts md5HashPromise migration — for the LocalFile
slice.
- _directoryCreationQueue refactor — works; not premature.
- webdav-api file split into smaller modules — follow-up.
Follow-up testing not yet run by Claude (handover gates):
- Full npm test suite (two timezone variants).
- Full E2E.
- Manual round-trip against a real WebDAV / Nextcloud server (PUT
with If-Match rev, 412 conflict path, 401 reauth path, 404 fresh-
client bootstrap).
* 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>
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.
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.
Bundle ical.js into the plugin and use it for the read path. The hand-rolled
parser stays for getById/updateIssue/deleteIssue. Anchor the sync window to
start-of-UTC-day so in-progress events stay visible, count only emitted
occurrences toward the per-event safety cap, and isolate per-VEVENT failures
so one malformed event can't drop the rest. Compound id uses '#occ=<ms>' so
a server-controlled href cannot collide with the occurrence delimiter.
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
- 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).
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.
* test(e2e): update Show/hide task panel button label
The TOGGLE_DETAIL_PANEL translation was renamed from "Show/Hide
additional info" to "Show/hide task panel" in dd789d2, but e2e
selectors still referenced the old string, causing every test that
opens the task detail panel to time out.
https://claude.ai/code/session_011mX4Pr24S42svmA5j7fRux
* 18.2.1
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: update electron to v41.1.1
* feat(start-app): clear GPU cache on Electron version change for Linux
* fix(window-decorators): disable custom window title bar for GNOME
This fixes some issues when moving and resizing the window
* refactor: Migrate url.format() (DEP0116) to file:// path approach
* refactor(start-app): remove deprecated protocol.registerFileProtocol in start-app.ts
* feat(electron-builder): update gnome content snap to gnome-42-2204 for improved compatibility and update flatpak permissions
* fix(window-decorators): improve handling of custom window title bar for GNOME
* feat(start-app): implement fallback to X11 in Snap if gnome-42-2204 runtime is unavailable
* chore: update electron to v41.1.1
* chore(package-lock): remove unused dependencies from package-lock.json
* fix(electron): move snap ozone-platform switch before app ready event
app.commandLine.appendSwitch() must be called before Chromium
initializes — after the ready event fires the GPU backend is already
running and the switch is a no-op. Move the Snap X11 fallback (defense-
in-depth for missing gnome-42-2204 runtime) to run synchronously at
startup, alongside the existing gtk-version and speech-dispatcher
switches.
* fix(electron): align IS_GNOME_DESKTOP detection with preload.ts logic
The original implementation only matched Ubuntu's GNOME session
(XDG_CURRENT_DESKTOP contains both 'gnome' AND 'ubuntu'), missing plain
GNOME on Fedora, Arch, etc. The preload.ts isGnomeDesktop() already
used the correct approach: check four environment variables with OR
logic. Align common.const.ts to match, preventing a split-brain where
the main process and renderer disagree on whether the user is on GNOME
— which would produce no title bar at all on non-Ubuntu GNOME desktops.
* fix(electron): restore v41 bump lost in merge and gate title-bar toggle
- Re-apply electron 41.2.0 + minimatch 10.2.5 override (master's 0e9218bd
reverted the dependabot bump back to 37.10.3 while this branch's
merge-base still contained 41.2.0, so the pre-merge diff was empty).
- Regenerate root package-lock.json accordingly.
- Drop unrelated esbuild additions from plugin-dev sub-lockfiles.
- misc-settings-form: gate isUseCustomWindowTitleBar on IS_ELECTRON &&
!IS_GNOME_DESKTOP so the toggle does not appear in the web/PWA build.
---------
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)