- Replace the `client.refusedChallengers = 0` post-summary reset with a
dedicated `summaryLogged: boolean` flag. The reset muddied the
field's documented "lifetime count" semantics for any future reader
observing the closing socket. The flag dedupes the `ws.on('close')`
re-entry without lying about the count.
- Refresh the stale `RECONNECT_COOLDOWN_MS` doc that still described
the non-sliding semantics from before fbdedf597e.
- Short-circuit the WS-429 path normalize on `statusCode === 429` in
setErrorHandler so the split+replace doesn't run on the 99%+ of
error responses that aren't 429.
- Drop the redundant `cid.length > 0` guard in `isValidClientId` —
the trailing regex already requires `+` (≥1 char).
- Route the websocket.routes spec simulator through `isValidClientId`
so it can't drift from production validation order again.
- Add a heartbeat-path regression test for the storm-summary dedup
(the explicit-eviction path was already covered by fbdedf597e).
The 5s cooldown introduced in ec1f09c85b kept the incumbent socket from
being evicted by a single challenger, but the gate was anchored to the
incumbent's connectedAt — so after 5s of a sustained pre-18.6.0
reconnect storm, the next challenger evicted the incumbent and the
cycle restarted every ~5s. Production logs confirmed: the same clientId
appeared in both "Reconnect within cooldown" WARNs and "Replacing stale
connection" INFOs within ~200ms.
Make the cooldown a sliding window via a single cooldownUntil field:
each refused challenger pushes it forward, so eviction only resumes
after RECONNECT_COOLDOWN_MS of quiet (no challengers). Sustained storm
keeps the incumbent forever; genuine network-blip reconnect after the
storm subsides still recovers.
Cut the per-attempt log spam:
- First refusal per incumbent emits the WARN; subsequent refusals are
silently counted on the ConnectedClient.
- On incumbent removal, log a single INFO summary with the total count
and incumbent lifetime. Zero the counter after logging so the
inevitable ws.on('close') re-entry (triggered by removeConnection's
own ws.close) cannot double-fire the summary.
- WS-route 429s downgraded to debug in setErrorHandler (storm tail, not
actionable). Exact-match on /api/sync/ws (path + query strip) so
future sibling routes do not silently inherit debug level.
Stop one bad client from draining shared-NAT quota: the WS route's
rate-limit keyGenerator now keys on \${ip}:\${clientId} instead of ip
alone. Per-IP amplification stays bounded by the server-wide 500/15min
cap. Extract isValidClientId into sync.const so the keyGenerator and
the route handler share one definition; length check runs before the
regex to reject multi-MB clientId probes cheaply.
Tests cover sliding window under sustained storm, one-WARN +
summary-on-removal, no-summary on clean close, and the
double-summary regression on the explicit-eviction path.
The focusin auto-switch added in #6578 set selectedTaskId to the clicked
task before the toggle button's click handler ran, making isSelected()
true and inverting the toggle to close. Skip the auto-switch when focus
came from the toggle button itself — the click handler owns that action.
* feat(focus-mode): auto-start break after manual session completion
* refactor(focus-mode): clean up manual break flow and update overtime semantics
* refactor(focus-mode): revert unintended Flowtime auto-break behavior
* docs(focus-mode): restore offerFlowtimeBreakOnSessionEnd$ rationale comment
The v3 revert removed the comment block documenting why this effect
listens to endFlowtimeSession rather than pauseFocusSession (which is
fired by sync-stop, idle, and the regular pause button). Restoring it
to match master.
---------
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Once a session is switched to Flowtime mid-run, only the Flowtime button
remained — a no-op single-button selector. The breathing dot and count-up
clock already convey the active mode, and users can't switch back to a
fixed-duration mode mid-session.
Hardens the shortcut: blocks alt/shift modifiers, skips when the
event target is an input/textarea/contenteditable or text is
selected, and guards against the focus-recovery path copying a
stale title. Matches on `ev.code === 'KeyC'` so the shortcut still
fires on non-Latin layouts, mirroring the browser's native copy.
Pre-18.6.0 clients reconnect immediately on the 4009 eviction. With a
per-origin clientId reused across rapid reconnects, addConnection evicted
the incumbent and accepted the challenger, emitting 4009 every cycle ->
self-sustaining reconnect storm (observed ~32 evictions/sec fleet-wide,
each triggering a client download, saturating the VM).
Add a 5s reconnect cooldown: if a still-OPEN socket for the clientId was
accepted < RECONNECT_COOLDOWN_MS ago, refuse the challenger (4008) and
keep the incumbent untouched. The incumbent is never evicted, so the
server stops emitting 4009 and the loop loses its fuel. A dead/closing
incumbent bypasses the cooldown, so a genuine network-blip reconnect
still recovers.
Prisma's default pool is host-core-scaled and silently consumes the
entire postgres max_connections cap. Document the required
?connection_limit=N&pool_timeout=20 in env.example so operators and
deploy.sh keep the bound (host .env is not in the repo).
Prisma's default connection pool is ~(host_cpu_count * 2), read from host
cores not the container cpu limit. On the production VPS that silently
consumed the entire postgres max_connections=40 cap, leaving zero
headroom for psql, migrations, the old-ops cleanup job, or the reconnect
stampede after an in-place crash recovery -> sustained 'sorry, too many
clients already', WS reconnect storms and 429s.
Raise max_connections to 120 (120 x 4MB work_mem + 384MB shared_buffers
stays well inside the 1.5g mem_limit). The app must also bound its pool
via DATABASE_URL ?connection_limit=60 (host .env, not in repo).
_sync() returned SyncStatus.InSync for every successful sync, so the
sync button always showed "Already in sync" even when ops were
downloaded or uploaded (discussion #7196). Compute whether the sync
moved any data from the existing download/upload results and return
SyncStatus.UpdateRemote when it did, InSync only when nothing changed.
Reuses existing enum + translated strings; no new i18n key or snackbar.
Broaden the post-sync SuperSync encryption-prompt guard from
'=== InSync' to '\!== HANDLED_ERROR' so it still fires after a sync
that changed data.
Append a short channel marker to the (display-only) version string so bug
reports and the config footer reveal which build a user runs, e.g.:
win nsis W portable P MS Store MS
mac dmg D App Store MAS
linux AppImage AI snap SN flatpak FP deb/rpm L
android Play A F-Droid AF ios I web WB
Desktop channels are detected in the Electron process (process.platform,
process.mas/windowsStore, APPIMAGE/SNAP/FLATPAK_ID) via a shared helper
reused by the tray-GUID logic and exposed sync over the preload bridge
(no IPC, getAppVersionStr stays synchronous). Mobile/web are detected in
the frontend. deb vs rpm is not distinguishable at runtime, so both
report L. No caller semver-compares this string.
PR #6194 changed en ADD_TASK_BAR.DUE_BUTTON value Due->Schedule but
left all locale files untranslated. The i18n script only fills
empty values, so 18 locales kept showing the old due/deadline word
(it: Scadenza, de: fällig, etc.). Retranslate DUE_BUTTON to mean
schedule/plan in those 18 locales.
PR #7650 (b80e81f3e3) regenerated package-lock.json with npm that did
not honor the app-builder-lib minimatch override, reverting it from
10.1.1 to 3.1.5/5.1.9/9.0.9. This desynced the lockfile from
package.json, breaking every `npm ci` build (SuperSync Docker image,
root Dockerfile, plugin-tests workflow).
Regenerate lockfile with npm install --package-lock-only so the
overrides block is applied again. Verified with npm ci --dry-run.
* feat(habit-tracker): grey out non-selected days in specific-days streak mode
* refactor(habit-tracker): optimize date parsing and fix streak mode logic
* feat(habit-tracker): fix disabled day interactions and add tests
GH Action will incorrectly mark discussions without the question-answer
format as answered when that is not possible. The action should be fixed
but restricting to the correct categories will also avoid this problem.
* feat(focus-mode): show flowtime and countdown timer in browser tab title and removes app name during timer
* fix(focus-mode): correct flowtime break browser title behavior
* feat(nav): add keyboard shortcut to toggle sidebar compact/full mode
Ctrl+B toggles the sidebar between compact and full mode, matching
VS Code's "Toggle Primary Side Bar Visibility". Uses the existing
signal-trigger pattern (LayoutService -> MagicSideNavComponent effect).
Shortcut is configurable in Settings > Keyboard Shortcuts.
* fix(nav): prevent spurious sidebar toggle on viewport resize
---------
Co-authored-by: agncr <agncr@users.noreply.github.com>
The currentTaskChange handler destructured { current, previous } from
the hook payload with no nullish guard, unlike its sibling handlers
(taskCreated/taskComplete/taskUpdate) which all check payload first.
A nullish payload threw TypeError into PluginHooksService.dispatchHook,
logged as a plugin hook handler error. Guard the payload before
destructuring, matching the existing handler pattern.
sharp is a native module that fails in F-Droid's offline/restricted
build environment. The prior partial fix only moved it to
optionalDependencies but left it in package-lock.json, so the build
still pulled it in. Remove sharp from package.json and the lockfile
entirely (replicating the proven #6637 fix), and lazy-install it on
demand only in the dev-only marketing-screenshot scripts that need it.
Closes#7542
* feat(tasks): auto add tasks with deadlines today to Today view
* fix(tasks): preserve schedule when completing tasks
* fix(tasks): cover scheduled completion edge cases
* chore(deps)(deps): bump cloudflare/wrangler-action from 3.15.0 to 4.0.0 (#7653)
Bumps [cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action) from 3.15.0 to 4.0.0.
- [Release notes](https://github.com/cloudflare/wrangler-action/releases)
- [Changelog](https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md)
- [Commits](9acf94ace1...ebbaa15849)
---
updated-dependencies:
- dependency-name: cloudflare/wrangler-action
dependency-version: 4.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps)(deps): bump the github-actions-minor group with 4 updates (#7652)
Bumps the github-actions-minor group with 4 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner), [browser-actions/setup-chrome](https://github.com/browser-actions/setup-chrome), [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) and [github/codeql-action](https://github.com/github/codeql-action).
Updates `step-security/harden-runner` from 2.19.1 to 2.19.3
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](a5ad31d6a1...ab7a9404c0)
Updates `browser-actions/setup-chrome` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/browser-actions/setup-chrome/releases)
- [Changelog](https://github.com/browser-actions/setup-chrome/blob/master/CHANGELOG.md)
- [Commits](4f8e94349a...2e1d749697)
Updates `anthropics/claude-code-action` from 1.0.111 to 1.0.123
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](fefa07e9c6...51ea8ea73a)
Updates `github/codeql-action` from 4.35.3 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e46ed2cbd0...9e0d7b8d25)
---
updated-dependencies:
- dependency-name: step-security/harden-runner
dependency-version: 2.19.3
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions-minor
- dependency-name: browser-actions/setup-chrome
dependency-version: 2.1.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions-minor
- dependency-name: anthropics/claude-code-action
dependency-version: 1.0.123
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions-minor
- dependency-name: github/codeql-action
dependency-version: 4.35.5
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(plugins): allow iframe-only plugin installs
* fix(config): validate start of next day boundary
* fix(sync): support separate Nextcloud login name
Refs #7617
* test(tasks): add scheduled completion round trips
* fix(focus-mode): allow active timers to switch to flowtime
* test(tasks): expand completion replay matrix
* perf(sync): use indexed findFirst for op-download minSeq
Prisma operation.aggregate({ _min: { serverSeq } }) compiles to MIN()
over a `SELECT ... OFFSET 0` subquery. The OFFSET is a Postgres planner
optimization fence, so the (user_id, server_seq) index could not serve a
first-row seek and the query degraded to a per-user O(N) scan. Under a
client reconnect stampede this ran minutes inside the 60s interactive
download transaction, blowing the tx timeout (500s) and exhausting the
connection pool (cascading upload+download failures).
Replace it with findFirst ordered by serverSeq asc, which compiles to
ORDER BY server_seq ASC LIMIT 1 — a guaranteed index seek on the existing
unique (user_id, server_seq) index. Behaviour-preserving: same minSeq
value and same null-when-empty semantics.
Update both consuming specs (operation-download.service, gap-detection)
to the two-findFirst-call shape and add a regression test asserting the
indexed query is used and the aggregate path is not reintroduced.
* fix(supersync): use 127.0.0.1 in caddy healthcheck to avoid ::1 refusal
busybox wget in caddy:2.11-alpine resolves `localhost` to ::1 first, but Caddy binds its admin API to IPv4 127.0.0.1:2019. The healthcheck got "connection refused" and marked a healthy Caddy (serving prod traffic with valid TLS) as unhealthy, making deploy.sh report a false "Container startup failed\!".
* test(focus-mode): update skip break e2e assertion
* test(focus-mode): update skip break expectation
* docs(supersync): align caddy admin healthcheck note to 127.0.0.1
Comment-only follow-up to the docker-compose healthcheck fix: the Caddyfile note still said localhost:2019; align it to the literal IP the probe now uses.
* test(focus-mode): restore mode selector locator
* refactor(tasks): make deadline auto-planning atomic
* test(focus-mode): restore mode selector locator
* fix(tasks): prevent duplicate subtask links
* fix(tasks): remove noisy warning chips
* fix(schedule): position over-budget badge correctly in day panel
* feat(tasks): auto add tasks with deadlines today to Today view
* refactor(tasks): make deadline auto-planning atomic
* Checkpoint deadline auto-plan review work
* fix(tasks): make deadline auto-planning atomic
* fix(tasks): clear orphaned remindAt when deadline auto-plan clears dueWithTime
Overdue tasks moved to Today via deadline auto-planning had their
dueWithTime cleared but kept remindAt, leaving a reminder anchored to a
time the task no longer has. Clear remindAt in the same atomic reducer
pass (one op), matching the planTasksForToday/dismissReminderOnly
convention. Document the auto-plan policy and deliberate decisions on
getDeadlineAutoPlanDecision.
* perf(tasks): O(1) Today-membership lookups in deadline auto-plan
Multi-review follow-up:
- getDeadlineAutoPlanDecision takes ReadonlySet instead of array; the
defensive effect now passes its accumulating Set directly and hoists a
Set for the due-task filters, removing the O(N*M) array-includes scan
on the date-rollover path.
- Drop redundant isTodayWithOffset import; reuse the local
getDateStrWithOffset helper (identical under the positive-finite
timestamp guard).
- Un-export isTaskDueTodayBySchedule (file-internal only).
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(schedule): add Shift/Ctrl+Arrow minute stepping to time inputs
Shift+Arrow steps by 5 min, Ctrl/Cmd+Arrow by 15 min — hardcoded to match
calendar app conventions and keep the feature self-contained.
* fix(schedule): prevent hour-segment jumping on modifier+Arrow steps
Always apply minute-based stepping via stepTimeString() regardless of
which segment is focused, by blocking the native browser step with
preventDefault().
* fix(schedule): wire TimeStepDirective into dialogs and address review feedback
- Apply spTimeStep to time inputs in dialog-deadline and dialog-schedule-task
- Import TimeStepDirective in both dialog components
- Guard disabled/readOnly inputs to avoid mutating non-interactive fields
- Add readOnly/disabled early-return to directive
- Expand directive spec: add ngModel integration test, preventDefault assertions
for all modifier paths, ev.repeat case; remove misleading duplicate hour-segment test
- Revert unrelated t.const.ts key reordering and en.json wording change
- Document HH:MM-only precondition in stepTimeString
* test(time-step): rename misleading repeat-key test
Name claimed the directive ignores ev.repeat keydowns, but it has
no repeat guard and the assertion verifies a held key still steps.
Rename to match the actual asserted behavior.
---------
Co-authored-by: agncr <agncr@users.noreply.github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Comment-only follow-up to the docker-compose healthcheck fix: the Caddyfile note still said localhost:2019; align it to the literal IP the probe now uses.
busybox wget in caddy:2.11-alpine resolves `localhost` to ::1 first, but Caddy binds its admin API to IPv4 127.0.0.1:2019. The healthcheck got "connection refused" and marked a healthy Caddy (serving prod traffic with valid TLS) as unhealthy, making deploy.sh report a false "Container startup failed\!".
Prisma operation.aggregate({ _min: { serverSeq } }) compiles to MIN()
over a `SELECT ... OFFSET 0` subquery. The OFFSET is a Postgres planner
optimization fence, so the (user_id, server_seq) index could not serve a
first-row seek and the query degraded to a per-user O(N) scan. Under a
client reconnect stampede this ran minutes inside the 60s interactive
download transaction, blowing the tx timeout (500s) and exhausting the
connection pool (cascading upload+download failures).
Replace it with findFirst ordered by serverSeq asc, which compiles to
ORDER BY server_seq ASC LIMIT 1 — a guaranteed index seek on the existing
unique (user_id, server_seq) index. Behaviour-preserving: same minSeq
value and same null-when-empty semantics.
Update both consuming specs (operation-download.service, gap-detection)
to the two-findFirst-call shape and add a regression test asserting the
indexed query is used and the aggregate path is not reintroduced.
The op-log validation system (src/app/op-log/validation/) handles all
data repair at sync checkpoints. The legacy DataRepairService had no
callers anywhere in the app.
The outer attemptRecovery catch already logs the thrown Error, but the
console timeline doesn't make it obvious that the failure was a typia
validation rejection (vs. an IDB error or anything else). Log a
distinguishable line with the typia error count and any cross-model
error before throwing so support can grep for it.
The startup hydration path called validateAndRepair(), which opens a
native confirm()/alert() dialog when typia flags state. On Windows this
steals renderer focus and leaves shortcuts/clicks dead until the window
is minimized and restored — exactly the symptom in #7631.
Switch hydration to binary validateState() (no dialogs) and treat
failures as non-fatal: dispatch the data so the UI is live, but skip
the snapshot save at Checkpoint C so a corrupted state is not cached
for next boot. Also gate two destructive paths with the same validator:
refuse to import legacy data that fails validation, and refuse to
clear the migration backup unless the migrated snapshot validates.
Drop the dead _repairMutex field and its guards.