Commit graph

20507 commits

Author SHA1 Message Date
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
d29c061646 fix(tasks): move collapsed subtasks label to i18n
The collapsed-subtasks button rendered a hardcoded '+ N sub tasks'
string, inconsistent with 'subtasks' used everywhere else and missing
from the i18n files. Externalize it to F.TASK.CMP with separate keys
for the hide-all and hide-done variants.

Closes #7618
2026-05-15 16:51:50 +02:00
Johannes Millan
f344abfc64 fix(sync): probe WebDAV base root in connection test (#7617)
testConnection() did a PROPFIND only on the configured sync folder.
On first-time setup that folder does not exist yet (created lazily on
first upload), so the server returned 404 and every correctly-configured
new Nextcloud/WebDAV user saw "Connection test failed".

Probe the WebDAV base root instead: it is reachable and auth-checked for
any valid server/credentials, while a wrong username/base path (404) or
bad password (401) still correctly fails. Add regression tests covering
the base-root probe, wrong-base 404, and the bad-credentials safety
invariant.
2026-05-15 16:49:17 +02:00
Johannes Millan
3e6d494f67 feat(ui): use rounded-square shape for task done-toggle
Replace the always-on background circle in the shared done-toggle
component with a slightly rounded square (rx/ry 5.5 on the 24-unit
viewBox). Rename the .done-circle SCSS selector to .done-shape;
stroke, opacity, hover and animation behavior are unchanged.

Applies everywhere the toggle is used (task list, planner).
2026-05-15 16:49:17 +02:00
Anzhao Liu
79138b1ad2
feat(schedule): add scheduling warnings for overlapping tasks and outside work hours (#7559)
* feat(tasks): highlight conflicting and out-of-hours tasks

* feat(work-view): warn when today's remaining estimate exceeds 8h

* feat(schedule): add real-time warnings in task scheduling dialog

* test(schedule): mock selectors for scheduling dialog specs

* fix(tasks): share scheduling conflict detection

* i18n(schedule): add scheduling hint translations

* fix(schedule): present scheduling hints as neutral info

* test(schedule): cover scheduling hint edge cases

* test(tasks): support scheduling selectors in shortcut specs
2026-05-15 14:09:52 +02:00
Gitoffthelawn
f6d838f248
Fix UI inconsistency (#7615) 2026-05-15 11:37:00 +02:00
Corey Newton
c39add913c
chore: combine and rename discussion pruning steps (#7610)
Re-ordered the steps to avoid a discussion that is marked as answered
just before the expiry to be false marked as OUTDATED.
Simplified messages.
Shortened waiting period for answered discussions.
2026-05-15 01:36:41 +02:00
Paul Tirk
1204380198
Plugin tag ids (#7582)
* feat(plugin-api): add tag ids to plugin field mapping

* feat(plugin-sync): handle tag ids in plugin sync adapter

* feat(plugin-sync): handle tag ids in plugin issue provider adapter

* feat(plugin-sync): add tags to two way sync

* fix(plugin-sync): add deep equality comparison for arrays

* refactor(plugin-sync): extract sortTagLabels utility and refactor sync adapters

* fix(two-way-sync): trigger sync on addTagToTask action

* fix(plugin-sync): harden tag synchronization

* fix(plugin-sync): preserve provider-owned baselines

---------

Co-authored-by: johannesjo <johannes.millan@gmail.com>
2026-05-15 01:36:07 +02:00
Miklos
6f5e174b54
fix(electron): use template tray icons on macOS (#7609)
The macOS tray icon was loaded as a static black or white PNG chosen
once at startup from `nativeTheme.shouldUseDarkColors`, so it stayed
black on a dark menu bar (or white on a light one) after the appearance
changed.

Use the `-l` (black-on-transparent) icons on macOS and mark them as
template images. macOS then auto-inverts them for the current menu bar
appearance, including the highlight state when the menu is open and
accessibility inverted-colors. Linux and Windows behavior is unchanged;
`--force-dark-tray` is now a no-op on macOS because template images make
manual forcing unnecessary.
2026-05-14 22:59:09 +02:00
Corey Newton
69fd0e9cb4
chore: try to fix stale deepwiki status (#7608) 2026-05-14 22:49:43 +02:00
Johannes Millan
9222646de8 Merge branch 'feat/can-we-help-them-fcad86'
* feat/can-we-help-them-fcad86:
  feat(plugin-automations): add taskStarted/taskStopped triggers and removeTag action
2026-05-14 21:14:14 +02:00
Johannes Millan
f94a46decf feat(plugin-automations): add taskStarted/taskStopped triggers and removeTag action
Adds two new triggers and a new action to the bundled automations plugin,
along with the host-side and validator changes needed to make them
reliable end to end.

Plugin

- New triggers TriggerTaskStarted / TriggerTaskStopped that fire on timer
  start / stop. Switching from task A to task B emits taskStopped(A) and
  then taskStarted(B), so a rule subscribed to either trigger gets the
  full lifecycle. The trigger descriptions document this explicitly.
- New ActionRemoveTag mirroring ActionAddTag, with a shared resolveTagId
  helper covering id/title lookup, missing-tag warning, and idempotent
  short-circuiting.
- Import goes through a new RuleRegistry.addRules(rules[]) + addRules
  message, so the host's 1 call/sec persistDataSynced rate limit no
  longer rejects multi-rule imports.
- Validators in core/rule-registry.ts and utils/rule-validator.ts share a
  set of `as const` arrays guarded by a TS _AssertEq exhaustiveness check
  against the union types in types.ts. The arrays are duplicated rather
  than exported from types.ts because that would turn types.ts into a
  shared runtime chunk and break plugin.js (which the host evaluates via
  `new Function`, not as an ES module).
- Import validator no longer requires a truthy rule name — the UI saves
  empty-named rules, the load-path accepts them, and the import path
  needed to match.
- ActionDialog gets a removeTag placeholder. Manifest's hooks list is
  updated to include the hooks the plugin actually registers
  (taskCreated, currentTaskChange) so the permission disclosure UI is
  accurate.

Host

- plugin-hooks.effects.ts onCurrentTaskChange$ now observes
  selectCurrentTask directly instead of only setCurrentTask /
  unsetCurrentTask actions. This catches transitions through reducer
  paths that don't dispatch those actions (loadAllData, deleteProject,
  bulk task delete via the shared CRUD meta-reducer).
- The payload changes from the raw new Task to { current, previous },
  matching the long-declared CurrentTaskChangePayload shape. The effect
  uses pairwise on the selector and filters same-id pairs at the event
  boundary (not via distinctUntilChanged on the source) so the
  `previous` task always carries the latest snapshot — including
  in-place updates a plugin made while the task was running. Without
  this, addTag-on-start / removeTag-on-stop only worked every other
  cycle because `previous` reflected the pre-mutation snapshot.

Other plugins consuming currentTaskChange (voice-reminder, api-test-plugin)
read payload.current instead of the raw Task.
2026-05-14 21:14:00 +02:00
Johannes Millan
b192f66de0 fix(plugins): restore plugin dialog backgrounds 2026-05-14 20:45:59 +02:00
Johannes Millan
eca2d3ec80 chore(funding): drop broken repositoryUrl.wellKnown line 2026-05-14 17:21:39 +02:00
Johannes Millan
eb54601512 build: remove .well-known 2026-05-14 17:14:03 +02:00
Johannes Millan
915b5c7f1a chore: add FLOSS/fund funding.json manifest 2026-05-14 16:43:26 +02:00
Johannes Millan
99807f88e8 fix(tasks): restore ArrowRight focusing into detail panel
The recently added task-host focus guard inside _focusFirst() and
focusItem() blocked the legitimate keyboard flow: pressing ArrowRight
calls showDetailPanel() -> focusSelf(), then the panel's auto-focus
bailed because the task host was focused. A second ArrowRight then
navigated to the next task instead of moving focus into the panel.

The #6578 focus-stealing protection in _focusOnTaskIdChange already
handles task-list navigation, making the inner guard redundant.
2026-05-14 15:24:53 +02:00
Johannes Millan
ece168fc6b Merge branch 'feat/sometimes-on-mobile-we-get-unable-to-9c9cbd'
* feat/sometimes-on-mobile-we-get-unable-to-9c9cbd:
  refactor(sync): tighten unable-to-resolve pattern and clarify retry doc
  fix(sync): widen transient-error detection and lengthen retry backoff
2026-05-14 14:57:08 +02:00
Johannes Millan
54dbc683a8 refactor(sync): tighten unable-to-resolve pattern and clarify retry doc
Address review nits on 058f92e972: anchor the retryable-upload regex
to "unable to resolve host" so future SuperSync op-graph rejection
strings (e.g. "Unable to resolve parent revision") can't be flipped
from permanent to retryable; rename RETRY_BACKOFF_MS to
RETRY_BACKOFF_BASE_MS (it is the linear base, not the total); clarify
the JSDoc that 4.5s is the inter-attempt sleep budget, not wall-clock.
2026-05-14 14:55:58 +02:00
Johannes Millan
058f92e972 fix(sync): widen transient-error detection and lengthen retry backoff
Android UnknownHostException after Doze-mode resume produced a raw
"Unable to resolve host" snackbar because the post-retry error message
didn't match isRetryableUploadError's patterns, so it skipped the
NetworkUnavailableSPError wrap. Bump the native HTTP retry backoff from
1s/2s to 1.5s/3s (~4.5s budget) to better tolerate post-Doze DNS
staleness, and add the Android phrasing to both transient-error checks
so the error now surfaces as the translated network-warning snackbar.
2026-05-14 14:49:34 +02:00
Johannes Millan
d659ea9d4d fix(theme): apply velvet sidenav blur on inner element, not host
backdrop-filter on magic-side-nav establishes a new containing block for
its fixed-positioned children (.nav-sidenav drawer and .nav-backdrop-mobile
overlay). On mobile the host shrinks to width:0, collapsing both children
off-screen so the drawer never opens — same trap that was fixed for
liquid-glass. Move background + backdrop-filter to the inner .nav-sidenav.

Add a guard comment on the component :host and a "Authoring Themes"
section in docs/styling-guide.md so future themes don't reintroduce this.
2026-05-14 14:41:08 +02:00
Benjamin
07ec51b1a5
feat(plugins): add onReady() API with IPC ping + fix consent write delay (#7578)
* feat(plugins): add onReady() API with IPC ping + fix consent write delay #7326

- Remove setTimeout(5000) from _getNodeExecutionConsent; write consent immediately
- Add plugin.onReady(fn) to PluginAPI — fires after plugin.js evaluation and IPC bridge confirmation
- Add _pingNodeBridge() in plugin.service.ts with 3-attempt retry (1s, 2s delays)
- Add triggerReady() and pingNodeBridge() to PluginRunner
- Show snack + set error state if IPC bridge unavailable after retries
- Add NODE_EXECUTION_BRIDGE_UNAVAILABLE translation key
- Add focused tests for onReady, triggerReady, pingNodeBridge, consent persistence
- Update plugin-development.md with onReady usage and nodeExecution guidance

* test(plugins): fix unused variable lint errors in spec files

* fix(plugins): guard triggerReady on instance.loaded; fix doc numbering

* fix(plugins): remove _triggerReady from public API, route ping via bridge, add retry tests

* fix(electron): add paths for @sp/sync-providers subpath exports (node moduleResolution compat)

* fix(plugins): centralize onReady, tear down runtime on activation error, add iframe onReady

Address review on #7578:
- All plugin load paths (startup, upload, reload, lazy) now go through _fireOnReady,
  ensuring the IPC ping + onReady fire on every successful load — not just lazy.
- activatePlugin error path now unloads the plugin runtime (hooks, buttons, side
  effects) before setting status='error', preventing partially-running plugins.
- Iframe PluginAPI now exposes onReady (fires on next microtask after plugin.js
  evaluates), matching the host-side contract for typed iframe plugins.

* fix(plugins): clean up half-loaded plugins on onReady error, test real retry util

Self-review followups:
- _fireOnReadyWithCleanup wraps the 3 non-activatePlugin load paths and tears
  down the plugin (unloadPlugin + remove from list + status='error' + snack)
  if the IPC ping or onReady callback throws. Previously, those paths only
  logged and rethrew, leaving partially-running plugins.
- Extracted retry loop into pure pingWithRetry utility; spec now exercises
  production code instead of an inline-replicated stub. Removed the old
  plugin-ping-node-bridge.spec.ts which was just testing its own copy of
  the logic.
- Documented iframe onReady semantic (fires on microtask, no ping) in both
  the source comment and docs/plugin-development.md, since cold-boot is not
  a concern for iframe plugins (rendered on demand).

* ci(plugins): use npm i for root install to tolerate override drift

The root lockfile pins app-builder-lib's transitive minimatch via the
`overrides` field. npm 10.9.7 (bundled with Node 22 in setup-node@v6)
flags this as drift and fails `npm ci`, while npm 11 accepts it.
ci.yml's main test job uses `npm i`, which tolerates the drift without
mutating the lockfile on disk.

Plugin-Tests has been red on every PR since 2026-05-08 for this reason.
The inner `npm ci` for plugin-specific deps stays strict.

* fix(plugins): make onReady optional, assert callback isolation in spec

- packages/plugin-api/src/types.ts: mark onReady? optional on the public
  PluginAPI interface so existing plugin TypeScript typings (and any
  third-party PluginAPI implementations) remain assignable after upgrade.
  The host runtime already treats onReady as optional (no-op if no
  registration callback is provided), so this aligns the type with the
  actual contract.

- src/app/plugins/plugin-runner.spec.ts: the previous isolation test only
  asserted that triggerReady() resolved for both plugins; it would still
  pass if triggerReady fired every registered callback. The updated test
  wires per-plugin Jasmine spies through globalThis (the same context the
  plugin code's `new Function` runs in) and asserts call counts before
  and after each triggerReady, actually proving isolation.

* refactor(plugins): test real consent logic; scope startup snacks; tighten ping timeout

Address review feedback on PR #7578:

- Extract consent decision into pure `decideNodeExecutionConsent` util so the
  spec exercises real code instead of a reimplemented stub. Delete the
  stub-based plugin-consent.spec.ts and plugin-fire-on-ready.spec.ts (the
  latter was orchestration glue already covered by plugin-runner.spec.ts and
  ping-with-retry.util.spec.ts).
- Reduce per-ping timeout 5000ms -> 1500ms. Worst-case cold-boot bridge-down
  detection drops from ~17s to ~7.5s; in-process vm script returning true
  doesn't need 5s.
- Add PLUGIN_LOAD_FAILED translation wrapping plugin name + error. Strip the
  now-redundant pluginName from NODE_EXECUTION_BRIDGE_UNAVAILABLE.
- Scope activation-failure snack to manual activations only — startup
  auto-activation failures stay silent (plugin tile shows error state).
  _handleReadyFailure still snacks unconditionally since onReady failure
  leaves a partially-loaded runtime that the user needs to see.

---------

Co-authored-by: Benjamin <1159333+benjaminburzan@users.noreply.github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-05-14 13:41:01 +02:00
Johannes Millan
087b9dd43f
refactor(sync): post-extraction review cleanup of @sp/sync-core and @sp/sync-providers (#7595)
* refactor(sync): tighten extracted package surfaces

Combined polish from the post-extraction review:

- sync-core: strip NgRx-shaped types from EntityConfig/EntityRegistry;
  expose host extensions via generic param. Move StateSelector,
  PropsStateSelector, SelectByIdFactory, SelectById, EntityUpdateLike,
  EntityAdapterLike to a new app-side entity-registry-host.types.ts.
- sync-core: mark OpType.SyncImport/BackupImport/Repair as @deprecated;
  hosts should use createFullStateOpTypeHelpers().
- sync-providers: resolve provider.types.ts vs provider-types.ts
  duplication; inline implementation into the dashed canonical name.
- sync-providers: drop unused root barrel and "." export; consumers
  already use focused subpath barrels (/dropbox, /webdav, etc.).
- sync-providers: replace wildcard "@sp/sync-providers/*" tsconfig path
  alias with 11 explicit subpath entries matching package.json exports;
  deep-internal imports now fail at typecheck.
- sync-providers: move @sp/sync-core from dependencies to
  peerDependencies (kept in devDependencies for tests).
- both packages: add composite: true to enable project references;
  introduce tsconfig.build.json overlay so tsup DTS bundler still works.
- gitignore: ignore **/*.tsbuildinfo composite outputs.

* refactor(sync-core): prune 47 unused barrel exports

Removes exports with zero consumers outside the package. Source files
are unchanged; only the public barrel is trimmed. Covers compression
helper classes, sync-file-prefix error/config types, replay coordinator
internals, remote-apply result types, upload/download planning option
and plan types, ports misc, conflict-resolution helper types, and
sync-import-filter decision types.

* refactor(sync-core): drop unused encryption migration path

decryptWithMigration and DecryptResult had no host consumer; they
exposed a structural-migration entry point ("here is your ciphertext
re-encrypted under Argon2id") that nothing in the codebase reads. The
side-channel setLegacyKdfWarningHandler — which IS used — stays.

encryptWithDerivedKey/decryptWithDerivedKey lose their export keyword
and remain as module-internal helpers; encrypt/decrypt/encryptBatch/
decryptBatch still call them. Wire format and legacy-fallback semantics
are unchanged, so existing ciphertext continues to decrypt.

Test imports for compression and sync-file-prefix specs now go via
their source files instead of the trimmed barrel.

* fix(sync-providers): bound dropbox token refresh to single retry; share md5 rev helper

The five hand-rolled token-refresh blocks in Dropbox.{getFileRev,
downloadFile, uploadFile, removeFile, listFiles} recursed on themselves
after refresh. If the post-refresh call still saw a token error (real
case: the refresh token itself was revoked), the recursion would not
terminate. Consolidated into a single _withTokenRefresh helper that
attempts the call, refreshes once on a token error, retries once, then
lets the outer 401 classifier surface AuthFailSPError.

Same log message, same _isTokenError discriminator, same refresh call.
Same five sites still apply their post-call non-token error mapping
(NoRev, InvalidData, RemoteFileNotFound, path-not-found swallow, etc.).

Also extracts md5 content-rev computation duplicated between
LocalFileSyncBase._getLocalRev and WebdavApi._computeContentHash into a
shared file-based/content-rev.ts; both call sites preserve their own
error wrapping at the boundary.

* refactor(sync): split oversized super-sync and conflict-resolution

sync-providers: extract request-ID hashing from super-sync.ts (1017 ->
918 lines) into a new request-id.ts. The helpers were free functions
already in disguise (none referenced this), so the move is mechanical.
HTTP plumbing (_doWebFetch/_doNativeFetch/_fetchApi*) stays as private
methods — it transitively touches 12 instance members and would need
either a wide context object or a separate http-client collaborator
class to extract cleanly. Left as a follow-up.

sync-core: split conflict-resolution.ts into three cohesive files:
- entity-frontier.ts now owns buildEntityFrontier and
  adjustForClockCorruption (per-entity vector-clock domain).
- extractEntityFromPayload and extractUpdateChanges move to
  operation.types.ts next to the existing extractActionPayload.
- conflict-resolution.ts keeps deep-equality, LWW planning,
  partitioning, and identical-conflict detection.

Public barrel exports unchanged; tests now import the moved symbols
from their new homes.

* refactor(sync-core): drop redundant OperationStorePort

OperationStorePort overlapped with RemoteOperationApplyStorePort on the
two state-transition methods (markSynced/markApplied,
markRejected/markFailed) and had zero non-structural consumers — the
only implementer was OperationLogStoreService, which already exposes
the three methods as its own public surface. Removing the port leaves
the service contract intact and removes the verb-pair confusion noted
in the post-extraction review.

Spec contract test still drives the same state transitions; only the
local typing of the test fixture changes from the deleted interface to
Pick<OperationLogStoreService, ...>.

* refactor(sync-providers): decouple SuperSync provider from SP-specific host

Two coupling leaks the package shouldn't carry:

1. SUPER_SYNC_DEFAULT_BASE_URL was an implicit fallback inside
   SuperSyncProvider — an SP-specific URL baked into a "framework-
   agnostic" package. Make defaultBaseUrl a required SuperSyncDeps
   field; the host factory supplies the SP default. The constant stays
   exported as a suggested default for hosts targeting the SP-hosted
   server.

2. Consumers that wanted the WebSocket path had to do
   `provider as unknown as SuperSyncProvider` to call
   getWebSocketParams. Introduce SuperSyncWebSocketAccess interface +
   isSuperSyncWebSocketAccess structural guard; SuperSyncProvider
   implements it. sync-wrapper.service drops its cast in favor of the
   guard.

super-sync-restore.service still casts to SuperSyncProvider for the
restore path — same pattern would solve it, but out of scope here.

* test(sync-providers): extract shared test helpers and prefer barrels

Adds tests/helpers/sync-logger.ts and tests/helpers/credential-store.ts
to centralize the noopLogger and CredentialStore mocks that were copy-
pasted across 8 spec files. createStatefulCredentialStore covers the
"load/upsert/clear with state" cases; createMockCredentialStore covers
bare vi.fn() ports. Spec sites that needed a unique mockResolvedValue
chain it after the helper, preserving behavior 1:1.

Also migrates 5 spec files from deep ../src/<file> paths to the
matching sub-barrel (../src/webdav, /http, /super-sync, /platform) for
symbols already exported there. No new barrel exports added — internal
types (WebDavHttpAdapter, WebdavApi, DropboxApi, etc.) stay on deep
paths because they are intentionally not part of the public surface.

super-sync.spec.ts keeps its own credential/logger mocks (special
__asPort wrapper and vi.spyOn against the live NOOP_SYNC_LOGGER) that
the generic helpers cannot reproduce without bloat.

* test(sync): pin vector-clock pruning, error-meta privacy, and sync-import edges

Fills three test gaps surfaced by the post-extraction review:

- vector-clock pruning correctness across clocks: 4 cases pinning that
  pruning legitimately flips GREATER_THAN to CONCURRENT/LESS_THAN when
  the dropped keys are still present in the comparison clock. This is
  the documented behavior (compareVectorClocks is intentionally not
  pruning-aware); the protocol handles flips server-side via the
  rejected-ops retry loop. preserveClientIds case also covered.

- error-meta privacy boundary: 22 new cases covering urlPathOnly (strip
  query/fragment/userinfo, preserve host+path+port, leave non-URLs
  intact) and errorMeta (no leakage of headers, response bodies, OAuth
  tokens, signed-URL params, user emails, or attached error fields).
  Real negative assertions (.not.toContain), not shape checks.

- sync-import-filter edge cases: 8 cases covering empty clocks on
  either side, op clock listing the import client at 0, same-client
  with equal counter (pinning the strict-greater-than boundary), and
  different-client knowledge above the import counter.

sync-core 195 -> 207 tests, sync-providers 319 -> 341 tests; no
production code changed.

* style(sync-core): format sync-file-prefix.spec import line

* fix(sync): address package review feedback
2026-05-14 13:06:08 +02:00
johannesjo
7f96181738 fix(pwa): avoid startup stalls during network changes 2026-05-14 12:41:09 +02:00
johannesjo
362847036d fix(sync): handle wrapped backup encryption import 2026-05-14 12:41:09 +02:00
johannesjo
518ceb6f20 fix(android): prevent restored legacy launch mode 2026-05-14 12:41:09 +02:00
Het Savani
252e6bea10
fix(tasks): persist collapsed section state across project switches (#7600) 2026-05-14 12:13:02 +02:00
Het Savani
f7f8056c50
feat(focus-mode): show pomodoro timer in browser tab title (#7579)
* feat(focus-mode): show pomodoro timer in browser tab title

* fix(focus-mode): handle overtime and translate browser title labels
2026-05-14 12:01:16 +02:00
adminlip
deab927bb7
fix: capitalize strings for consistency (#7596)
- Capitalize "all tasks completed" → "All tasks completed" in en.json
- Fix "Url" → "URL" acronym in error handler

Closes #4649

Co-authored-by: adminlip <adminlip@users.noreply.github.com>
2026-05-14 11:59:25 +02:00
adminlip
1db9b87897
Improve wording capitalization (#7597)
Co-authored-by: adminlip <adminlip@users.noreply.github.com>
2026-05-14 11:57:58 +02:00
Gitoffthelawn
0b13cbf175
Fixed UI errors (#7599)
Fixed several errors I noticed and made a couple improvements.
2026-05-14 11:57:11 +02:00
johannesjo
c0a667ac42 fix(worklog): reload worklog on context change so metrics page is per-project
Activity heatmap on /tag|project/:id/metrics showed identical data across
projects and a stale year selector. WorklogService.worklogData$ was gated
by _archiveUpdateTrigger$, which only fired on initial load, manual refresh,
and navigation to /worklog, /daily-summary, or /quick-history — not /metrics.
Inside the trigger, take(1) on activeWorkContext$ snapshotted the context
once per emission, so even when it did fire it could capture a stale id.
Combined with shareReplay({bufferSize:1, refCount:true}), the heatmap kept
rendering whichever context was last loaded.

Add 'metrics' to the URL filter, and replace take(1) with a live
activeWorkContext$ subscription gated by distinctUntilChanged on id so a
context switch (with no /metrics navigation in between) also reloads.
Apply the same shape to _quickHistoryData$.

Manual refreshes and URL triggers on the same context still reload because
each trigger emission opens a fresh inner subscription whose
distinctUntilChanged has no prior value.
2026-05-14 00:49:24 +02:00
johannesjo
5bc9d90ee0 chore(plugins): refresh community plugin star counts 2026-05-13 23:43:20 +02:00
Johannes Millan
001e9847b4 build(electron): scope tsc path aliases to package dist types
tsc -p electron/tsconfig.electron.json was resolving @sp/* aliases to
packages/*/src/*.ts, then transitively compiling those sources and
emitting .js next to each .ts (no outDir is set). Every electron-bearing
task (electron:build, npm start, build, dist) re-littered
packages/{sync-core,sync-providers,shared-schema}/src with stale .js
shadows; gitignore hid them from git but they still shadowed the .ts
sources for Vitest, silently breaking module mocks.

Point the aliases at dist/index.d.ts (and dist/* for subpaths) so tsc
consumes declarations only and never touches package source. Runtime
resolution is unaffected (Node uses the package.json exports).
2026-05-13 22:01:20 +02:00
Johannes Millan
6e9b11dd2e Merge branch 'feat/electron-preload-js-6-3kb-b045f8'
* feat/electron-preload-js-6-3kb-b045f8:
  build(electron): exclude uuidv7 and guard against future asar regressions
  build(electron): exclude dev/mobile deps from asar to shrink installer
2026-05-13 21:28:47 +02:00
Johannes Millan
ddc01ab6cf build(electron): exclude uuidv7 and guard against future asar regressions
uuidv7 is imported only from src/app/util/uuid-v7.ts; the Angular
bundler inlines it (verified via chunk-65AEYVPY.js.map source
attribution). Adding it to the three electron-builder configs drops
another ~80 KB from the asar.

Extend tools/verify-electron-requires.js to also flag bare require()
targets in electron/ that match a `\!**/<pkg>/**` exclusion in
electron-builder.yaml. Without this, adding `require('@noble/ciphers')`
or similar in electron/main.ts would pass dev (which resolves against
on-disk node_modules) and only crash with MODULE_NOT_FOUND on packaged
releases. The script is already invoked from .github/workflows/
electron-smoke.yml, so the new check runs in CI automatically.

Manual verification covers 14 cases (8 excluded, 6 allowed) including
@noble/ciphers, @noble/hashes, scoped-vs-bare resolution, and Node
built-ins.
2026-05-13 21:22:59 +02:00
Johannes Millan
24f8d390b6 ci(plugins): use npm i for root install to tolerate override drift
The root lockfile pins app-builder-lib's transitive minimatch via the
`overrides` field. npm 10.9.7 (bundled with Node 22 in setup-node@v6)
flags this as drift and fails `npm ci`, while npm 11 accepts it.
ci.yml's main test job uses `npm i`, which tolerates the drift without
mutating the lockfile on disk.

Plugin-Tests has been red on every PR since 2026-05-08 for this reason.
The inner `npm ci` for plugin-specific deps stays strict.
2026-05-13 21:22:02 +02:00
Johannes Millan
95b0d8e2f7 build(electron): exclude dev/mobile deps from asar to shrink installer
Adds 10 file patterns to the three electron-builder configs that prune
node_modules entries which are never required from electron/ at runtime:

- Capacitor + capawesome + capacitor-plugin-safe-area (mobile shim)
- sharp + @img native binaries (only used by tools/generate-*-icon.js)
- @lmdb (Nx cache) and @rollup (build tool) pulled in transitively
- @material-symbols (already compiled into the Angular CSS bundle)
- @noble/ciphers and hash-wasm (inlined into the Angular bundle by the
  bundler; the WASM payloads are base64-embedded)

Verified inlining via chunk-H6URPRRJ.js.map source attribution and by
locating distinctive WASM/AES fingerprints inside the Angular chunks.
2026-05-13 21:15:40 +02:00
Johannes Millan
0b21524e29 Merge branch 'feat/production-sync-server-is-full-of-these-b7ab49'
* feat/production-sync-server-is-full-of-these-b7ab49:
  fix(supersync): dedupe WS connections by clientId to evict stale sockets
2026-05-13 19:59:40 +02:00
Johannes Millan
ef4cbff44b Merge branch 'feat/do-a-complete-review-of-the-extration-66f89a'
* feat/do-a-complete-review-of-the-extration-66f89a:
  fix(sync): correct error class names, JSDoc, and missed cache-clear
2026-05-13 19:59:36 +02:00
Johannes Millan
70dd6f9eca build: ignore files 2026-05-13 19:59:02 +02:00
Johannes Millan
87f092bee3 fix(sync): correct error class names, JSDoc, and missed cache-clear
Findings from a multi-agent review of the recent sync extraction:

- Seven error classes in @sp/sync-providers shipped with a leading
  space in `name`, and `UploadRevToMatchMismatchAPIError` was further
  truncated to ' UploadRevToMatchMismatchAP'. Consumers use instanceof
  so runtime behavior was preserved, but stack traces, error envelopes,
  and structured-log meta carried the broken names. Added a regression
  test asserting instance.name matches ErrCtor.name for all 14 classes.
- @sp/sync-core decryptBatch JSDoc claimed Argon2 errors are never
  silently masked as legacy fallbacks, contradicting the actual
  catch-and-decryptLegacy path. The fallback is part of the public
  wire-format contract; rewrote the comment to match the implementation
  and reference the module-level wire-format spec.
- SuperSyncEncryptionToggleService.enable/disableEncryption promised
  "Clear cache on success" but never called clearSessionKeyCache().
  Added the call on the success path in both methods, matching the
  pattern used by EncryptionPasswordChangeService and
  FileBasedEncryptionService.
- Removed packages/sync-core/tests/ports.spec.ts — 57 LOC of
  vi.fn().mockResolvedValue type-assertion tests with no production
  code under test. Port shapes are enforced at the host via
  `implements` clauses with real behavioral coverage in sibling specs.
2026-05-13 19:49:26 +02:00
Johannes Millan
85c7543f49 fix(supersync): dedupe WS connections by clientId to evict stale sockets
A reconnecting device kept appending alongside its stale entry instead of
replacing it, so userSet would fill to MAX_CONNECTIONS_PER_USER and reject
legitimate reconnects with 4008 until the 30s+10s heartbeat cycle caught up.
addConnection now evicts any existing entry with the same clientId before
adding the new socket; the cap still applies to genuinely distinct clientIds.
2026-05-13 19:48:43 +02:00
Johannes Millan
24cc99e2f8 test(sync): mock WebCrypto availability in snapshot upload spec 2026-05-13 17:39:32 +02:00
Johannes Millan
a0309ac21b Merge branch 'feat/do-a-very-careful-review-of-all-our-d21bcb'
* feat/do-a-very-careful-review-of-all-our-d21bcb:
  fix(supersync): guard snapshot retry idempotency lookup by userId
2026-05-13 17:30:17 +02:00
Johannes Millan
3a56d9595b fix(supersync): guard snapshot retry idempotency lookup by userId
The DUPLICATE_OPERATION->success conversion in the snapshot route looked
up the existing op by primary key alone. `isSameDuplicateOperation`
already enforces ownership upstream, so this can't be tripped today, but
keeping the userId filter on the route's own lookup keeps the
idempotency conversion correct even if that upstream invariant ever
changes. Switches `findUnique({id})` to `findFirst({id, userId})`;
`id` is still the primary key so the plan is unchanged.
2026-05-13 17:29:43 +02:00
Johannes Millan
18cae275f5 fix(sync): harden encryption batching and SuperSync abort timer
- super-sync: keep the 75s abort timer alive across response.text() on
  non-OK responses so a stalled error body still triggers AbortError.
- decryptBatch: hold derived keys in a batch-local map. Previously
  Phase 3 read from the LRU session cache, which could evict entries
  mid-batch when the input contained more unique salts than the cache
  could hold (SESSION_DECRYPT_CACHE_MAX_SIZE = 100), crashing on the
  non-null assertion. Adds a 120-item regression test.
- session cache: replace the 32-bit djb2 password identifier with a
  length-prefixed full-password key (injective). A djb2 collision
  silently returned a key derived from a different password, producing
  undecryptable ciphertext on subsequent encrypts.
- docs: bless salt-per-session-per-password semantics explicitly in the
  encryption.ts JSDoc and the sync-core README so future readers and
  tests know IV uniqueness — not salt uniqueness — is the AES-GCM
  invariant being preserved.
2026-05-13 17:26:55 +02:00
Johannes Millan
4e1ace0786 refactor(sync): remove encryption test mocks 2026-05-13 17:26:55 +02:00
Johannes Millan
1d08cb9bc4 refactor(sync): split encryption primitives 2026-05-13 17:26:55 +02:00
Johannes Millan
4b856b3411 refactor(sync-core): extract encryption primitives 2026-05-13 17:26:55 +02:00