Bump Next.js to 15.1.9 + doc nav handles in-page anchors + help_info_menu

Three changes that fold into the v1.2.2 release PR:

1. AppImage: bump Next.js 15.1.6 -> 15.1.9 (CVE-2025-55182)
   GHSA-9qr9-h5gf-34mp / React2Shell is a pre-auth RCE in React Server
   Components when Server Functions deserialize attacker payloads. The
   ProxMenux Monitor ships Next.js in `output: "export"` mode behind
   Flask on :8008, so there is no runtime Next.js server and no
   "use server" directive in the source tree — the exploitable path is
   not reachable. Bumping to 15.1.9 anyway because OpenVAS and similar
   scanners flag the version string from the JS bundle regardless of
   architecture; raising the floor removes false-positive noise across
   every install. Reported by @rost43 in #219.

2. web/components/ui/doc-navigation.tsx: handle sidebar entries that
   point to in-page anchors. The Storage Share Manager sidebar has
   entries for `/docs/storage-share#host` and
   `/docs/storage-share#lxc-net` as section headers, but
   usePathname() does not include the hash so every visit collapsed
   to the parent page. As a result Next/Previous on /docs/storage-share
   stayed stuck at #host, and Next from .../lxc-mount-points/ pointed
   back at #host instead of #lxc-net. Read window.location.hash on
   mount (and on hashchange) and try the pathname+hash match before
   falling back to the pathname-only lookup. SSR hydrates with an
   empty hash and refreshes once mounted — brief render before
   hydration is the same as the previous behaviour, so no regression.

3. scripts/help_info_menu.sh: user-side improvement (mirrored from
   develop).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-06-01 22:31:12 +02:00
parent 3b2665c4ac
commit 2f24de2592
6 changed files with 4496 additions and 25 deletions
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
097e2344675d4b21f1dd18c531c956c299a6507fbc3d0c9695418063581ba2b0 149acc8644e7830ddccda97faa452f36f9a820c507e2c6b54fc9a7e51b9b4297
+4429
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -49,7 +49,7 @@
"geist": "^1.3.1", "geist": "^1.3.1",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"next": "15.1.6", "next": "15.1.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19", "react": "^19",
"react-day-picker": "9.8.0", "react-day-picker": "9.8.0",
+43 -1
View File
@@ -9,6 +9,7 @@
import { Link, usePathname } from "@/i18n/navigation" import { Link, usePathname } from "@/i18n/navigation"
import { ChevronLeft, ChevronRight } from "lucide-react" import { ChevronLeft, ChevronRight } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useEffect, useState } from "react"
import { sidebarItems } from "@/components/DocSidebar" import { sidebarItems } from "@/components/DocSidebar"
interface DocNavigationProps { interface DocNavigationProps {
@@ -55,6 +56,26 @@ export function DocNavigation({ className }: DocNavigationProps) {
const tNav = useTranslations("docNav") const tNav = useTranslations("docNav")
const tSidebar = useTranslations("docSidebar") const tSidebar = useTranslations("docSidebar")
// Capture the URL hash (`#host`, `#lxc-net`, …) on the client so we
// can disambiguate Previous/Next when a single doc page hosts several
// sidebar entries via in-page anchors (Storage Share Manager is the
// canonical case: /docs/storage-share + /docs/storage-share#host +
// /docs/storage-share#lxc-net are three distinct sidebar items but a
// single physical page; usePathname() returns the same string for
// all three because the fragment is not part of the path).
//
// SSR can't see the hash, so we hydrate with an empty string and
// refresh on mount + on hashchange. The brief render before
// hydration just shows the navigation as if the user were at the
// parent page — same behaviour as before this fix, so no regression.
const [hash, setHash] = useState("")
useEffect(() => {
const sync = () => setHash(window.location.hash || "")
sync()
window.addEventListener("hashchange", sync)
return () => window.removeEventListener("hashchange", sync)
}, [])
const tItem = (i18nKey: string | undefined, fallback: string) => { const tItem = (i18nKey: string | undefined, fallback: string) => {
if (!i18nKey) return fallback if (!i18nKey) return fallback
try { try {
@@ -102,9 +123,30 @@ export function DocNavigation({ className }: DocNavigationProps) {
// bar showed "Next: Introduction" everywhere regardless of the route. // bar showed "Next: Introduction" everywhere regardless of the route.
const stripTrailingSlash = (s: string) => (s !== "/" ? s.replace(/\/+$/, "") : s) const stripTrailingSlash = (s: string) => (s !== "/" ? s.replace(/\/+$/, "") : s)
const normalizedPathname = stripTrailingSlash(pathname) const normalizedPathname = stripTrailingSlash(pathname)
const currentPageIndex = allPages.findIndex(
// Match attempt order:
// 1. pathname + hash (e.g. /docs/storage-share#host) — exact match
// against sidebar items that intentionally point to an in-page
// anchor as the "current location" for navigation purposes.
// 2. pathname alone — the regular case, no anchor in the URL.
//
// Without step 1, every anchor visit collapsed to the parent page
// and Next/Previous walked from there — so on /docs/storage-share#host
// the bottom bar offered the same #host as Next (no movement) and on
// /docs/storage-share/lxc-mount-points/ Next pointed back at #host
// because the entire flat list got indexed from position 0.
const effectivePath = normalizedPathname + hash
let currentPageIndex = -1
if (hash) {
currentPageIndex = allPages.findIndex(
(page) => stripTrailingSlash(page.href) === effectivePath,
)
}
if (currentPageIndex === -1) {
currentPageIndex = allPages.findIndex(
(page) => stripTrailingSlash(page.href) === normalizedPathname, (page) => stripTrailingSlash(page.href) === normalizedPathname,
) )
}
const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null
const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null