mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Merge pull request #1265 from MBombeck/fix/v1441-next-security
Release v1.44.1
This commit is contained in:
13 files changed
+391
-137
No files matched your search
+16
-16
@@ -14,9 +14,10 @@ updates:
|
||||
# single bumps: patch and minor updates travel together and pass the
|
||||
# same checks once. Majors stay single so each gets its own review.
|
||||
#
|
||||
# The Next.js line travels alone: while a 16.4 patch is still being
|
||||
# re-measured, a red Next bump inside the group holds every harmless
|
||||
# update in it red as well.
|
||||
# The Next.js line travels alone. Its bumps are the ones that move
|
||||
# URL state after history traversal (see the note on 16.4 below), and a
|
||||
# red Next bump inside the group would hold every harmless update in
|
||||
# it red as well.
|
||||
production-weekly:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
@@ -32,19 +33,18 @@ updates:
|
||||
exclude-patterns:
|
||||
- "eslint-config-next"
|
||||
- "@next/bundle-analyzer"
|
||||
ignore:
|
||||
# Next 16.4.0 breaks URL state after history traversal: with a sheet
|
||||
# opened through `?doc=`, Back leaves it open, and `[` / `]` sometimes
|
||||
# do not move `?day=`. Reproduced in three end-to-end journeys (3 of 40
|
||||
# repeated runs on 16.4.0, 0 of 80 on 16.3.8 with everything else
|
||||
# equal). 16.3.8 carries every published security fix, so the line
|
||||
# waits for a 16.4 patch; the two packages that move with it wait too.
|
||||
- dependency-name: "next"
|
||||
versions: ["16.4.0"]
|
||||
- dependency-name: "eslint-config-next"
|
||||
versions: ["16.4.0"]
|
||||
- dependency-name: "@next/bundle-analyzer"
|
||||
versions: ["16.4.0"]
|
||||
# No ignore for the Next.js line any more. 16.4.0 lost URL state after
|
||||
# history traversal: with a sheet opened through `?doc=`, Back left it
|
||||
# open, and `[` / `]` sometimes did not move `?day=` (3 of 40 repeated
|
||||
# runs of three end-to-end journeys, 0 of 80 on 16.3.8). 16.4.2 does the
|
||||
# same (the day keys lost a step in 2 of 20 runs locally, the `?doc=`
|
||||
# journey failed once in CI). Both were ours to fix: the sheet followed
|
||||
# `useSearchParams`, which 16.4 updates after the write it copies, and a
|
||||
# day step wrote the URL before Next had adopted the previous step, whose
|
||||
# late adoption then put the older URL back. The sheet now follows the
|
||||
# address bar and a step waits for the layer. Measured after both fixes:
|
||||
# 200 of 200 repeated runs of the three journeys, and another 80 of 80
|
||||
# each for the `?doc=` and day-key journeys.
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [1.44.1] — 2026-10-22
|
||||
|
||||
### Security
|
||||
|
||||
- **Next.js 16.4.2.** The framework fixed two critical advisories that
|
||||
allow unauthenticated remote code execution, one in `use cache` functions
|
||||
that return typed arrays (GHSA-673q-mjm6-r5xr) and one in the image
|
||||
optimizer when it handles SVG (GHSA-2464-fhr4-w9xw), and a high-severity
|
||||
denial of service through Server Actions (GHSA-p3m5-59mp-jv5r). HealthLog
|
||||
uses neither `use cache` nor the image component, and it defines no Server
|
||||
Actions and refuses requests that call one. Every instance runs the same
|
||||
framework, though, so it updates anyway.
|
||||
- **The image optimizer is off.** Nothing in HealthLog renders through it,
|
||||
so `/_next/image` now answers 404 instead of processing whatever image a
|
||||
request names. Icons and logos load as before.
|
||||
- **nodemailer 10.1.7** fixes several advisories in how email addresses are
|
||||
parsed (GHSA-8c8m-p3m8-cjg8 among them).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`[` and `]` move the open day on every press.** Pressed quickly in a
|
||||
row, one press could be lost and the day ended one short. A step now
|
||||
starts from the day in the address bar and waits until the previous one
|
||||
is shown.
|
||||
- **Back closes a document opened from the vault.** Pressed right after
|
||||
opening a document, Back could leave its sheet open. The sheet now follows
|
||||
the address bar directly.
|
||||
|
||||
### Upgrade notes
|
||||
|
||||
- None. Pull the new image and restart.
|
||||
|
||||
## [1.44.0] — 2026-10-22
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.44.0
|
||||
version: 1.44.1
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -35,6 +35,16 @@ const nextConfig: NextConfig = {
|
||||
// still covers the full program.
|
||||
typescript: { tsconfigPath: "tsconfig.build.json" },
|
||||
poweredByHeader: false,
|
||||
// The image optimizer is off. Nothing renders through `next/image`: logos,
|
||||
// icons and avatars are plain `<img>` elements served from `public/` or
|
||||
// from an authenticated route. With the optimizer on, `/_next/image` still
|
||||
// fetched, decoded and re-encoded whatever local path a request named,
|
||||
// which is surface this app has no use for and where the framework has
|
||||
// shipped critical advisories. Off, Next answers that route with 404
|
||||
// before it reads a single parameter. `next-image-optimizer-guard.test.ts`
|
||||
// holds this: it fails when `next/image` is imported while the optimizer
|
||||
// is off, and when the optimizer comes back with SVG input allowed.
|
||||
images: { unoptimized: true },
|
||||
// v1.4.38.4 — expose the package.json version to the client bundle
|
||||
// so the `<VersionPoller>` can compare the shell-baked version
|
||||
// against the live `/api/version` response and trigger a self-heal
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.44.0",
|
||||
"version": "1.44.1",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"homepage": "https://healthlog.dev",
|
||||
@@ -79,8 +79,8 @@
|
||||
"jspdf-autotable": "^5.0.8",
|
||||
"lucide-react": "^1.56.0",
|
||||
"mmdb-lib": "^3.0.3",
|
||||
"next": "16.3.8",
|
||||
"nodemailer": "10.1.0",
|
||||
"next": "16.4.2",
|
||||
"nodemailer": "10.1.7",
|
||||
"otpauth": "^9.5.2",
|
||||
"p-limit": "^7.3.3",
|
||||
"pg": "^8.23.1",
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.13.0",
|
||||
"@next/bundle-analyzer": "^16.3.8",
|
||||
"@next/bundle-analyzer": "16.4.2",
|
||||
"@playwright/test": "^1.64.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tesseract.js-data/deu": "1.0.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"@vitest/coverage-v8": "^5.0.4",
|
||||
"ajv": "^8.20.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.3.8",
|
||||
"eslint-config-next": "16.4.2",
|
||||
"knip": "^6.41.0",
|
||||
"lefthook": "^2.2.2",
|
||||
"pdf-parse": "^2.4.5",
|
||||
|
||||
Generated
+65
-91
@@ -120,11 +120,11 @@ importers:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
next:
|
||||
specifier: 16.3.8
|
||||
version: 16.3.8(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
specifier: 16.4.2
|
||||
version: 16.4.2(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
nodemailer:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0
|
||||
specifier: 10.1.7
|
||||
version: 10.1.7
|
||||
otpauth:
|
||||
specifier: ^9.5.2
|
||||
version: 9.5.2
|
||||
@@ -196,8 +196,8 @@ importers:
|
||||
specifier: ^4.13.0
|
||||
version: 4.13.0(playwright-core@1.64.0)
|
||||
'@next/bundle-analyzer':
|
||||
specifier: ^16.3.8
|
||||
version: 16.3.8
|
||||
specifier: 16.4.2
|
||||
version: 16.4.2
|
||||
'@playwright/test':
|
||||
specifier: ^1.64.0
|
||||
version: 1.64.0
|
||||
@@ -241,8 +241,8 @@ importers:
|
||||
specifier: ^9
|
||||
version: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-config-next:
|
||||
specifier: 16.3.8
|
||||
version: 16.3.8(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
|
||||
specifier: 16.4.2
|
||||
version: 16.4.2(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
|
||||
knip:
|
||||
specifier: ^6.41.0
|
||||
version: 6.41.0
|
||||
@@ -1075,13 +1075,6 @@ packages:
|
||||
resolution: {integrity: sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.4':
|
||||
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
||||
peerDependencies:
|
||||
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
|
||||
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.5':
|
||||
resolution: {integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
||||
@@ -1089,63 +1082,63 @@ packages:
|
||||
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
|
||||
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
|
||||
|
||||
'@next/bundle-analyzer@16.3.8':
|
||||
resolution: {integrity: sha512-8VBk5/pjANz7fxbQZrDwUMTAs598Vckb4PE7BSYrDIPthJskKCXEHOK+WEMgDD6X2xWlKjrXPo2r41N/G1azSw==}
|
||||
'@next/bundle-analyzer@16.4.2':
|
||||
resolution: {integrity: sha512-tpNTC/WZYsmmixg3MZfx5JUuTh+Sgryu4eKhe7leQRcXHabztqtTkPCQM1Bk1VsE0trZ3w7NUCC3Zf7SeIBVzg==}
|
||||
|
||||
'@next/env@16.3.8':
|
||||
resolution: {integrity: sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==}
|
||||
'@next/env@16.4.2':
|
||||
resolution: {integrity: sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==}
|
||||
|
||||
'@next/eslint-plugin-next@16.3.8':
|
||||
resolution: {integrity: sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==}
|
||||
'@next/eslint-plugin-next@16.4.2':
|
||||
resolution: {integrity: sha512-gCTWlw0e+Y6kajpFyKKgA195jUJCRhlzJE0lV7ufe8a+ghIsANWZ0/FiJwg6DuOG1+OvRbcZDcFBoRh3mNJx0g==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.3.8':
|
||||
resolution: {integrity: sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==}
|
||||
'@next/swc-darwin-arm64@16.4.2':
|
||||
resolution: {integrity: sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.3.8':
|
||||
resolution: {integrity: sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==}
|
||||
'@next/swc-darwin-x64@16.4.2':
|
||||
resolution: {integrity: sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.3.8':
|
||||
resolution: {integrity: sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==}
|
||||
'@next/swc-linux-arm64-gnu@16.4.2':
|
||||
resolution: {integrity: sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.3.8':
|
||||
resolution: {integrity: sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==}
|
||||
'@next/swc-linux-arm64-musl@16.4.2':
|
||||
resolution: {integrity: sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.3.8':
|
||||
resolution: {integrity: sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==}
|
||||
'@next/swc-linux-x64-gnu@16.4.2':
|
||||
resolution: {integrity: sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.3.8':
|
||||
resolution: {integrity: sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==}
|
||||
'@next/swc-linux-x64-musl@16.4.2':
|
||||
resolution: {integrity: sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.3.8':
|
||||
resolution: {integrity: sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==}
|
||||
'@next/swc-win32-arm64-msvc@16.4.2':
|
||||
resolution: {integrity: sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.3.8':
|
||||
resolution: {integrity: sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==}
|
||||
'@next/swc-win32-x64-msvc@16.4.2':
|
||||
resolution: {integrity: sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -3348,11 +3341,6 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.11.26:
|
||||
resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
baseline-browser-mapping@2.11.27:
|
||||
resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -3459,9 +3447,6 @@ packages:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001814:
|
||||
resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==}
|
||||
|
||||
caniuse-lite@1.0.30001815:
|
||||
resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==}
|
||||
|
||||
@@ -3961,8 +3946,8 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-next@16.3.8:
|
||||
resolution: {integrity: sha512-81vovwMe6NGnoFsl0KUJWzlS+y239i3fdsxs49gSEX3pDQW2cuKy+fxUPXNkQqqx6OCPheYPtBlSufc9aZ0k+w==}
|
||||
eslint-config-next@16.4.2:
|
||||
resolution: {integrity: sha512-P33vjcETMXc+qBjpF77TeJHZLzk6cPcTy8KjpxLucYB3mCIFIORMcZ+cBXcAiIYjE5RM7jyknEEOwMNdnx/ADQ==}
|
||||
peerDependencies:
|
||||
eslint: '>=9.0.0'
|
||||
typescript: '>=3.3.1'
|
||||
@@ -5177,11 +5162,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.19:
|
||||
resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.20:
|
||||
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -5199,8 +5179,8 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
next@16.3.8:
|
||||
resolution: {integrity: sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==}
|
||||
next@16.4.2:
|
||||
resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5241,8 +5221,8 @@ packages:
|
||||
resolution: {integrity: sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nodemailer@10.1.0:
|
||||
resolution: {integrity: sha512-fXW8AulB25aOTuwfRYV6lqHyfz2VM4rr4SGXH03mE7x5znEUpzL5mu6anARUpZN7744Ziwkmr4AxSLPJsBQspg==}
|
||||
nodemailer@10.1.7:
|
||||
resolution: {integrity: sha512-OQa6FDln5fLttmqSbjDiRYvFDOFNrBqfiOwHPlEbURil3b+vPB1v8xpi+Sp46drzNDlsTnNsS502vthmz56r4w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
non-error@0.1.0:
|
||||
@@ -7727,7 +7707,7 @@ snapshots:
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 1.0.11
|
||||
'@napi-rs/canvas-win32-x64-msvc': 1.0.11
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
'@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
'@emnapi/runtime': 1.10.0
|
||||
@@ -7748,44 +7728,44 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.4
|
||||
optional: true
|
||||
|
||||
'@next/bundle-analyzer@16.3.8':
|
||||
'@next/bundle-analyzer@16.4.2':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@next/env@16.3.8': {}
|
||||
'@next/env@16.4.2': {}
|
||||
|
||||
'@next/eslint-plugin-next@16.3.8(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))':
|
||||
'@next/eslint-plugin-next@16.4.2(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
|
||||
fast-glob: 3.3.1
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
|
||||
'@next/swc-darwin-arm64@16.3.8':
|
||||
'@next/swc-darwin-arm64@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.3.8':
|
||||
'@next/swc-darwin-x64@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.3.8':
|
||||
'@next/swc-linux-arm64-gnu@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.3.8':
|
||||
'@next/swc-linux-arm64-musl@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.3.8':
|
||||
'@next/swc-linux-x64-gnu@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.3.8':
|
||||
'@next/swc-linux-x64-musl@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.3.8':
|
||||
'@next/swc-win32-arm64-msvc@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.3.8':
|
||||
'@next/swc-win32-x64-msvc@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@noble/hashes@2.4.0': {}
|
||||
@@ -9625,7 +9605,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
'@emnapi/runtime': 1.10.0
|
||||
'@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
||||
'@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
||||
optional: true
|
||||
|
||||
'@unrs/resolver-binding-win32-arm64-msvc@1.12.2':
|
||||
@@ -9991,8 +9971,6 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.11.26: {}
|
||||
|
||||
baseline-browser-mapping@2.11.27: {}
|
||||
|
||||
bcrypt-pbkdf@1.0.2:
|
||||
@@ -10120,8 +10098,6 @@ snapshots:
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001814: {}
|
||||
|
||||
caniuse-lite@1.0.30001815: {}
|
||||
|
||||
canvg@3.0.11:
|
||||
@@ -10644,9 +10620,9 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@16.3.8(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
|
||||
eslint-config-next@16.4.2(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.3.8(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
|
||||
'@next/eslint-plugin-next': 16.4.2(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
|
||||
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
@@ -11907,8 +11883,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
nanoid@3.3.19: {}
|
||||
|
||||
nanoid@3.3.20: {}
|
||||
|
||||
napi-postinstall@0.3.4: {}
|
||||
@@ -11917,25 +11891,25 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next@16.3.8(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
|
||||
next@16.4.2(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
|
||||
dependencies:
|
||||
'@next/env': 16.3.8
|
||||
'@next/env': 16.4.2
|
||||
'@swc/helpers': 0.5.23
|
||||
baseline-browser-mapping: 2.11.26
|
||||
caniuse-lite: 1.0.30001814
|
||||
baseline-browser-mapping: 2.11.27
|
||||
caniuse-lite: 1.0.30001815
|
||||
postcss: 8.5.23
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.3.8
|
||||
'@next/swc-darwin-x64': 16.3.8
|
||||
'@next/swc-linux-arm64-gnu': 16.3.8
|
||||
'@next/swc-linux-arm64-musl': 16.3.8
|
||||
'@next/swc-linux-x64-gnu': 16.3.8
|
||||
'@next/swc-linux-x64-musl': 16.3.8
|
||||
'@next/swc-win32-arm64-msvc': 16.3.8
|
||||
'@next/swc-win32-x64-msvc': 16.3.8
|
||||
'@next/swc-darwin-arm64': 16.4.2
|
||||
'@next/swc-darwin-x64': 16.4.2
|
||||
'@next/swc-linux-arm64-gnu': 16.4.2
|
||||
'@next/swc-linux-arm64-musl': 16.4.2
|
||||
'@next/swc-linux-x64-gnu': 16.4.2
|
||||
'@next/swc-linux-x64-musl': 16.4.2
|
||||
'@next/swc-win32-arm64-msvc': 16.4.2
|
||||
'@next/swc-win32-x64-msvc': 16.4.2
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@playwright/test': 1.64.0
|
||||
sharp: 0.35.5(@types/node@26.6.5)
|
||||
@@ -11959,7 +11933,7 @@ snapshots:
|
||||
|
||||
node-releases@2.0.58: {}
|
||||
|
||||
nodemailer@10.1.0: {}
|
||||
nodemailer@10.1.7: {}
|
||||
|
||||
non-error@0.1.0: {}
|
||||
|
||||
@@ -12300,7 +12274,7 @@ snapshots:
|
||||
|
||||
postcss@8.5.23:
|
||||
dependencies:
|
||||
nanoid: 3.3.19
|
||||
nanoid: 3.3.20
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ try {
|
||||
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
|
||||
const CACHE_VERSION =
|
||||
(typeof self !== "undefined" && self.__APP_VERSION__) ||
|
||||
/* @sw-version-fallback */ "v1.44.0";
|
||||
/* @sw-version-fallback */ "v1.44.1";
|
||||
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* The image optimizer stays off while nothing uses it.
|
||||
*
|
||||
* `next.config.ts` sets `images.unoptimized`, which makes Next answer
|
||||
* `/_next/image` with 404 before it reads a parameter, so the route that
|
||||
* fetches, decodes and re-encodes images is not reachable at all. Two ways
|
||||
* to undo that quietly, and this pins both:
|
||||
*
|
||||
* 1. A component imports `next/image` (or `next/legacy/image`) while the
|
||||
* optimizer is off. It would render, unoptimized, and the next person
|
||||
* to notice the missing optimization would switch the optimizer back on
|
||||
* without the review this flag deserves. Using `next/image` means
|
||||
* revisiting this decision, so the import fails here first.
|
||||
* 2. The optimizer comes back on with `dangerouslyAllowSVG`. An SVG is a
|
||||
* document, not a picture, and the framework's own advisories have been
|
||||
* about exactly that input. Re-enabling the optimizer is allowed; doing
|
||||
* it with SVG input allowed is not.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
requireFloor,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SRC = join(ROOT, "src");
|
||||
|
||||
type ImagesConfig = {
|
||||
unoptimized?: boolean;
|
||||
dangerouslyAllowSVG?: boolean;
|
||||
};
|
||||
|
||||
async function imagesConfig(): Promise<ImagesConfig> {
|
||||
const mod = (await import("../../next.config")) as {
|
||||
default: { images?: ImagesConfig };
|
||||
};
|
||||
return mod.default.images ?? {};
|
||||
}
|
||||
|
||||
// Any import of the module: static, dynamic, `require`, side effect or
|
||||
// re-export. Two literals with one shape rather than one built from a string,
|
||||
// so nothing has to be escaped into a pattern.
|
||||
const NEXT_IMAGE_IMPORT =
|
||||
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)["']next\/(?:legacy\/)?image["']/m;
|
||||
const NEXT_LINK_IMPORT =
|
||||
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)["']next\/link["']/m;
|
||||
|
||||
/** Every non-generated, non-test source file under `src/` matching `re`. */
|
||||
function importers(re: RegExp): string[] {
|
||||
return walkSourceFiles(SRC, { floor: 3000 })
|
||||
.filter((rel) => !rel.startsWith("generated/"))
|
||||
.filter((rel) => !rel.includes("__tests__/"))
|
||||
.filter((rel) =>
|
||||
re.test(stripComments(readFileSync(join(SRC, rel), "utf8"))),
|
||||
);
|
||||
}
|
||||
|
||||
describe("next/image optimizer", () => {
|
||||
it("the import matcher recognises every form of the import", () => {
|
||||
// The sweep below passes when it matches nothing, so the matcher has to
|
||||
// be shown to match before an empty result means anything.
|
||||
for (const line of [
|
||||
'import Image from "next/image";',
|
||||
"import Image from 'next/legacy/image';",
|
||||
'const Image = await import("next/image");',
|
||||
'const Image = require("next/image");',
|
||||
'import "next/image";',
|
||||
'export { default } from "next/image";',
|
||||
]) {
|
||||
expect(NEXT_IMAGE_IMPORT.test(line), line).toBe(true);
|
||||
}
|
||||
for (const line of [
|
||||
"// `next/image` is wrong here",
|
||||
'const path = "/_next/image?url=x";',
|
||||
'import { foo } from "next/image-extra";',
|
||||
]) {
|
||||
expect(NEXT_IMAGE_IMPORT.test(stripComments(line)), line).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("the same sweep finds the imports of a sibling module", () => {
|
||||
// An empty result for `next/image` is only worth something if the walk,
|
||||
// the comment stripping and the matcher together do find real imports.
|
||||
// `next/link` is imported the same way all over the tree.
|
||||
requireFloor("next/link importers", importers(NEXT_LINK_IMPORT), 100);
|
||||
});
|
||||
|
||||
it("is off, and then nothing imports next/image", async () => {
|
||||
const images = await imagesConfig();
|
||||
if (images.unoptimized !== true) return;
|
||||
expect(importers(NEXT_IMAGE_IMPORT)).toEqual([]);
|
||||
});
|
||||
|
||||
it("is never on with SVG input allowed", async () => {
|
||||
const images = await imagesConfig();
|
||||
if (images.unoptimized === true) return;
|
||||
expect(images.dangerouslyAllowSVG ?? false).toBe(false);
|
||||
});
|
||||
|
||||
it("is off today", async () => {
|
||||
// The two cases above each skip on the other branch. This pins which
|
||||
// branch the tree is on, so flipping it is a visible change to this
|
||||
// file and not a silent switch between the two.
|
||||
expect((await imagesConfig()).unoptimized).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
*
|
||||
* - opening adds exactly one entry, so Back closes the layer;
|
||||
* - opening another day while one is open swaps it in place;
|
||||
* - stepping to the neighbouring day never adds an entry, however often;
|
||||
* - stepping to the neighbouring day never adds an entry, however often,
|
||||
* starts from the day in the URL and never passes today;
|
||||
* - closing a day the layer opened goes Back (the next Back leaves the
|
||||
* page); closing a deep link only drops the parameter and stays.
|
||||
*/
|
||||
@@ -57,6 +58,8 @@ function installFakeBrowser(initialUrl: string) {
|
||||
|
||||
let browser: ReturnType<typeof installFakeBrowser>;
|
||||
|
||||
const TODAY = "2026-01-10";
|
||||
|
||||
beforeEach(() => {
|
||||
browser = installFakeBrowser("/insights/blood-pressure?range=90");
|
||||
});
|
||||
@@ -88,18 +91,63 @@ describe("day layer history", () => {
|
||||
it("steps without adding history entries", async () => {
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
stepDay("2026-01-03", -1);
|
||||
stepDay("2026-01-02", -1);
|
||||
stepDay("2026-01-01", 1);
|
||||
stepDay(-1, TODAY);
|
||||
stepDay(-1, TODAY);
|
||||
stepDay(1, TODAY);
|
||||
expect(browser.length()).toBe(2);
|
||||
expect(browser.current()).toContain("day=2026-01-02");
|
||||
});
|
||||
|
||||
it("steps from the day in the URL, so quick steps in a row all count", async () => {
|
||||
// Nothing republishes the open day between these calls, which is what a
|
||||
// fast key repeat looks like while the layer is still rendering the
|
||||
// previous step. Each step must still move from where the last one went.
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
expect(stepDay(1, TODAY)).toBe("2026-01-04");
|
||||
expect(stepDay(1, TODAY)).toBe("2026-01-05");
|
||||
expect(browser.current()).toContain("day=2026-01-05");
|
||||
});
|
||||
|
||||
it("holds a step while the layer still shows another day, then makes it", async () => {
|
||||
// The layer publishes the day it shows only once Next has adopted the
|
||||
// URL. A step written before that can be put back by Next's own write,
|
||||
// so it waits for the layer and goes in one write when it catches up.
|
||||
const { openDay, publishOpenDay, stepDay } =
|
||||
await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
publishOpenDay("2026-01-03");
|
||||
expect(stepDay(-1, TODAY)).toBe("2026-01-02");
|
||||
// The layer still shows the 3rd: these two wait.
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(browser.current()).toContain("day=2026-01-02");
|
||||
publishOpenDay("2026-01-02");
|
||||
expect(browser.current()).toContain("day=2026-01-04");
|
||||
expect(browser.length()).toBe(2);
|
||||
publishOpenDay(null);
|
||||
});
|
||||
|
||||
it("never steps past today, however often it is asked", async () => {
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-09");
|
||||
expect(stepDay(1, TODAY)).toBe(TODAY);
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(browser.current()).toContain(`day=${TODAY}`);
|
||||
});
|
||||
|
||||
it("does nothing while no day is open", async () => {
|
||||
const { stepDay } = await import("../day-layer-controller");
|
||||
expect(stepDay(-1, TODAY)).toBeNull();
|
||||
expect(browser.current()).toBe("/insights/blood-pressure?range=90");
|
||||
});
|
||||
|
||||
it("closes a pushed day with Back, landing on the page without it", async () => {
|
||||
const { closeDay, openDay, stepDay } =
|
||||
await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
stepDay("2026-01-03", 1);
|
||||
stepDay(1, TODAY);
|
||||
closeDay();
|
||||
expect(browser.current()).toBe("/insights/blood-pressure?range=90");
|
||||
});
|
||||
@@ -107,7 +155,7 @@ describe("day layer history", () => {
|
||||
it("closes a deep link in place: the parameter goes, the page stays", async () => {
|
||||
browser = installFakeBrowser("/?day=2026-01-03");
|
||||
const { closeDay, stepDay } = await import("../day-layer-controller");
|
||||
stepDay("2026-01-03", -1);
|
||||
stepDay(-1, TODAY);
|
||||
expect(browser.current()).toBe("/?day=2026-01-02");
|
||||
closeDay();
|
||||
expect(browser.current()).toBe("/");
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import type { DateKey } from "@/lib/day/contract";
|
||||
import { DAY_QUERY_PARAM, type DateKey } from "@/lib/day/contract";
|
||||
|
||||
import {
|
||||
dayHistoryState,
|
||||
historyEntryOwnedByLayer,
|
||||
parseDayParam,
|
||||
shiftDateKey,
|
||||
withDayHref,
|
||||
} from "./day-url";
|
||||
@@ -100,6 +101,13 @@ export function publishOpenDay(date: DateKey | null): void {
|
||||
if (memory.open === date) return;
|
||||
memory.open = date;
|
||||
emit();
|
||||
// The layer has caught up with the URL: the steps that waited for it go
|
||||
// now, in one write, from the day it shows.
|
||||
const waiting = pendingStep;
|
||||
pendingStep = null;
|
||||
if (waiting !== null && date !== null) {
|
||||
stepDay(waiting.delta, waiting.today);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,9 +163,50 @@ export function openDay(date: DateKey, options: OpenDayOptions = {}): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Move the open day by `delta` calendar days, without a new history entry. */
|
||||
export function stepDay(from: DateKey, delta: number): DateKey {
|
||||
const next = shiftDateKey(from, delta);
|
||||
/**
|
||||
* Steps asked for while the layer had not yet shown the previous one. Kept
|
||||
* as one sum, with the `today` of the latest request.
|
||||
*/
|
||||
let pendingStep: { delta: number; today: DateKey } | null = null;
|
||||
|
||||
/**
|
||||
* Move the open day by `delta` calendar days, without a new history entry,
|
||||
* and never past `today`. Null when nothing is written now: no day is open,
|
||||
* the step would pass today, or it waits for the previous one.
|
||||
*
|
||||
* Two things keep quick steps (a key held down, `]` pressed twice) from
|
||||
* losing one.
|
||||
*
|
||||
* The step starts from the day in the URL, not from the day a caller last
|
||||
* rendered, which reaches it through `useSearchParams`, a render and an
|
||||
* effect and can be a step behind.
|
||||
*
|
||||
* And a step does not write while the layer still shows a different day
|
||||
* than the URL. The write goes past Next's router, which adopts it a render
|
||||
* later, and since Next 16.4 that render can land after a second write and
|
||||
* put the first one's URL back: `[` then `]` pressed quickly ended a day
|
||||
* short. So a step that finds the layer behind waits, and `publishOpenDay`
|
||||
* makes it as soon as the layer shows the day the URL names.
|
||||
*/
|
||||
export function stepDay(delta: number, today: DateKey): DateKey | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const from = parseDayParam(
|
||||
new URLSearchParams(window.location.search).get(DAY_QUERY_PARAM),
|
||||
today,
|
||||
);
|
||||
if (from === null) {
|
||||
pendingStep = null;
|
||||
return null;
|
||||
}
|
||||
const total = (pendingStep?.delta ?? 0) + delta;
|
||||
if (memory.open !== null && memory.open !== from) {
|
||||
pendingStep = { delta: total, today };
|
||||
return null;
|
||||
}
|
||||
pendingStep = null;
|
||||
const target = shiftDateKey(from, total);
|
||||
const next = target > today ? today : target;
|
||||
if (next === from) return null;
|
||||
jumpDay(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -348,8 +348,7 @@ function DayLayer() {
|
||||
const onStep = useCallback(
|
||||
(delta: number) => {
|
||||
if (date === null) return;
|
||||
if (delta > 0 && date >= today) return;
|
||||
stepDay(date, delta);
|
||||
stepDay(delta, today);
|
||||
},
|
||||
[date, today],
|
||||
);
|
||||
|
||||
@@ -31,7 +31,14 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/errors/error-toast";
|
||||
|
||||
@@ -110,6 +117,26 @@ const BULK_VISIT_OPTIONS = 12;
|
||||
const DOCUMENT_QUERY_ID_PATTERN = /^[a-zA-Z0-9_-]{1,40}(?![\s\S])/;
|
||||
const DOCUMENT_SELECTION_HISTORY_KEY = "__healthlogPushedDocumentSelection";
|
||||
|
||||
/**
|
||||
* The query string in the address bar, read at render time.
|
||||
*
|
||||
* The open sheet follows `?doc=`, and `useSearchParams` is not a faithful
|
||||
* copy of it: Next 16.4 applies the copy of a `pushState` later than the write,
|
||||
* and a Back pressed in between moves the address bar from `?doc=` to the
|
||||
* page without it while `useSearchParams` can read "no document" on both sides.
|
||||
* The sheet then saw no change and stayed open. `window.location` is never
|
||||
* behind; `popstate` re-reads it on every traversal, and every other render
|
||||
* (including the ones Next's own navigations cause) reads it afresh.
|
||||
*/
|
||||
function subscribeToTraversal(onChange: () => void): () => void {
|
||||
window.addEventListener("popstate", onChange);
|
||||
return () => window.removeEventListener("popstate", onChange);
|
||||
}
|
||||
|
||||
function readLocationSearch(): string {
|
||||
return window.location.search;
|
||||
}
|
||||
|
||||
export function documentSelectionHistoryState(
|
||||
documentId: string,
|
||||
): Record<string, unknown> {
|
||||
@@ -818,7 +845,13 @@ export function DocumentsView() {
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const handedOffDocumentRef = useRef<string | null>(null);
|
||||
|
||||
const rawDocParam = searchParams.get("doc");
|
||||
// Before hydration the server's view of the query stands in for the URL.
|
||||
const locationSearch = useSyncExternalStore(
|
||||
subscribeToTraversal,
|
||||
readLocationSearch,
|
||||
() => searchParams.toString(),
|
||||
);
|
||||
const rawDocParam = new URLSearchParams(locationSearch).get("doc");
|
||||
const docParam =
|
||||
rawDocParam !== null && DOCUMENT_QUERY_ID_PATTERN.test(rawDocParam)
|
||||
? rawDocParam
|
||||
@@ -837,14 +870,14 @@ export function DocumentsView() {
|
||||
|
||||
const openDetail = useCallback(
|
||||
(id: string) => {
|
||||
const href = documentSelectionHref(pathname, searchParams.toString(), id);
|
||||
const href = documentSelectionHref(pathname, window.location.search, id);
|
||||
if (href === null) return;
|
||||
handedOffDocumentRef.current = null;
|
||||
setDetailId(id);
|
||||
setDetailOpen(true);
|
||||
window.history.pushState(documentSelectionHistoryState(id), "", href);
|
||||
},
|
||||
[pathname, searchParams],
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const handleDetailOpenChange = useCallback((open: boolean) => {
|
||||
@@ -881,7 +914,7 @@ export function DocumentsView() {
|
||||
handedOffDocumentRef.current = closeDocumentSelectionAfterCoachHandoff(
|
||||
window.history,
|
||||
pathname,
|
||||
searchParams.toString(),
|
||||
window.location.search,
|
||||
docParam,
|
||||
handedOffDocumentRef.current,
|
||||
coachLaunch?.closeIntent,
|
||||
@@ -925,7 +958,6 @@ export function DocumentsView() {
|
||||
docParam,
|
||||
moduleEnabled,
|
||||
pathname,
|
||||
searchParams,
|
||||
]);
|
||||
|
||||
// Hover/focus intent prefetches the detail METADATA (never the blob —
|
||||
|
||||
@@ -192,12 +192,12 @@ export function GlobalShortcuts() {
|
||||
setShortcutsHelpOpen(true);
|
||||
return;
|
||||
case "day": {
|
||||
const date = state.openDay;
|
||||
if (date === null) return;
|
||||
if (state.openDay === null) return;
|
||||
event.preventDefault();
|
||||
// No day after today: the layer refuses it, so the step does too.
|
||||
if (action.delta > 0 && date >= state.today) return;
|
||||
stepDay(date, action.delta);
|
||||
// The step reads the day from the URL, which is ahead of
|
||||
// `openDay` while a previous step is still rendering, and refuses
|
||||
// a day after today itself.
|
||||
stepDay(action.delta, state.today);
|
||||
return;
|
||||
}
|
||||
case "day-toggle":
|
||||
|
||||
Reference in new issue
Block a user