5.0.8's maxLength cap was applied in combine(), where output is merged, but
not to the two arrays built before it runs: comma alternatives each got their
own full allowance and were concatenated with no running total, and padded
sequences never consulted maxLength at all. So a ~25 KB pattern still OOMs the
process -- fatally, past the reach of try/catch -- and a ~400 KB one blocks the
event loop for over two minutes. 5.0.9 bounds both as they are built.
Dev-only and transitive here: it reaches us as eslint -> minimatch@5 ->
brace-expansion, the only input it sees is our own lint globs, and it is not in
the shipped bundle. The ci.yml audit gate runs --omit=dev, so this never would
have failed CI; it surfaced through Dependabot.
The overrides floor is bumped alongside the lockfile so a clean install can't
resolve back to the vulnerable 5.0.8.
- postcss 8.5.15 -> 8.5.23 (GHSA-r28c-9q8g-f849, source-map path traversal)
- brace-expansion override ^5.0.7 -> ^5.0.8 (GHSA-mh99-v99m-4gvg, DoS)
react-router: pin react-router-dom to exact 7.18.1 (direct dep) and react-router
to 7.18.1 via overrides (transitive). 7.18.1 is the most-patched 7.x -- it clears
14 advisories that older 7.x releases carry, several reachable from a SPA (open-
redirect XSS in Link/useNavigate, route-matching DoS). The one remaining advisory,
GHSA-qwww-vcr4-c8h2, is RSC-mode-only; Bambuddy is a Vite SPA using BrowserRouter
with no RSC runtime (@react-router/server not installed), so the path is
unreachable. The only version that fully clears npm audit is the 8.3.0 major
(no react-router-dom 8.x exists; it needs migrating 50 import sites plus a React
peer bump), deferred as its own change.
Because a version pin can't stop npm from reporting the theoretical 7.11.0
downgrade as fixAvailable, the ci.yml (hard) and security.yml (nightly issue)
audit gates gain a narrow, documented allowlist keyed on the GHSA id. It resolves
the react-router-dom -> react-router advisory chain and stays fail-closed: a
different advisory on react-router still fails the gate, and an isSemVerMajor
guard drops the exemption the moment a non-major fix ships, forcing us to take it.
Both are transitive dev-only dependencies under eslint (via minimatch and
@eslint/eslintrc) with denial-of-service advisories (GHSA-3jxr-9vmj-r5cp,
GHSA-52cp-r559-cp3m). They are lint/build tooling and not part of the
shipped app, so no running install was exposed. npm audit fix wouldn't move
eslint to the patched releases on its own, so they are pinned through the
existing overrides block in package.json (brace-expansion ^5.0.7,
js-yaml ^4.3.0). npm audit now reports zero vulnerabilities; eslint runs clean.
Reporter (@zumik3-del, seconded by @unLieb) asked for three File Manager
improvements: recursive search, tags, and a markdown preview side panel.
This commit ships the two scoped ones; tags is held back gated on the
"give the issue a thumbs up" interest check Martin posted on the issue
because it's a much larger surface (M2M schema, CRUD endpoints, tag UI +
filter + autocomplete + i18n for the management surface) and isn't the
right call without a real demand signal.
1) Recursive search inside the selected folder.
Until now, selecting "Toys" and typing "robot" only found files
directly in Toys/ — anything under Toys/Cars/Race/ stayed invisible.
The page's client-side filter ran over a server-narrowed listing
(/library/files?folder_id=X is strict equality on folder_id), so the
client filter couldn't see what the listing never loaded.
list_files (backend/app/api/routes/library.py:1729+) gains a
recursive=true query param. When combined with folder_id, the route
walks library_folders.parent_id via a recursive CTE rooted at the
requested folder and returns every descendant folder's files in one
query. Recursive CTEs work on both SQLite >=3.8.3 (2014, well below
Bambuddy's runtime floor) and Postgres without dialect branching.
Default off so the existing folder-browsing call sites (Project /
Archive detail, the FE's no-search case) keep their narrow scope.
FE opts in only when both a folder is selected AND searchQuery is
non-empty (FileManagerPage.tsx — derived as searchExpandsSubfolders,
threaded through the useQuery key so the cache invalidates on
toggle). Small "Including subfolders" caption renders under the
search input when active so the user understands why a file from two
levels deep showed up.
2) Per-folder markdown description panel.
New endpoint GET /library/folders/{folder_id}/readme returns the
first .md file in the folder as {filename, content, truncated}.
Selection prefers README.md / readme.md / description.md
(case-insensitive via func.lower(filename) LIKE '%.md' + an
in-Python stem-preference sort), falls back to the
alphabetically-first *.md otherwise. 404 when no markdown is present
so the FE can hide the side panel — non-users pay no UI cost.
Bytes are clipped at 512 KiB (_README_BYTES_CAP) with a truncated
flag so the panel can warn the reader. UTF-8 decode uses
errors="replace" so one bad byte never blanks the panel.
New FolderReadmePanel.tsx fetches on folder-select and renders via
react-markdown@9 + remark-gfm@4 (tables, strikethrough, task lists).
Collapsible (default expanded), max-height 24rem with internal
scroll. react-markdown 9 doesn't render raw HTML by default — no
dompurify needed. Links open in a new tab with rel=noopener
noreferrer. Tailwind has no typography plugin in this project so
per-element components map h1/h2/h3/p/ul/ol/code/blockquote/table
to explicit utility classes that match the rest of the app.
Scope and permissions.
Both endpoints reuse the existing LIBRARY_READ_ALL / LIBRARY_READ_OWN
ownership-aware pair, so a viewer-tier user with read_own only sees
their own files in recursive listings and can only fetch the README of
folders containing their own files. No new permission, no DB migration.
The recursive CTE is a single SQL query — no N+1, no per-folder
round-trip, scales to deeply-nested model libraries.
Major version bump for the frontend build:
- vite ^7.3.2 -> ^8.0.16
- @vitejs/plugin-react ^5.1.1 -> ^5.2.0
Vite 8 swaps Rollup for Rolldown as the default bundler
(Rust-backed, same plugin contract). The bump also lifts the
transitive esbuild floor to 0.28.1, closing the last open
advisory in the audit chain.
vite.config.ts surface audited and unchanged:
- defineConfig, Connect type
- serveGcodeViewer configureServer middleware
- server.proxy with WebSocket upgrade for /api/v1/ws
- build.outDir / emptyOutDir / chunkSizeWarningLimit
- resolve.alias for @
- base: '/' regression guard from #1221
vitest@4.1.8 already accepts vite 8 in its peer range
(^6 || ^7 || ^8); no test-runner bump required.
Node floor for vite 8 is ^20.19.0 || >=22.12.0; CI Node 20.x
line satisfies this.
Not taken: plugin-react v6 — it requires
babel-plugin-react-compiler and @rolldown/plugin-babel as
peers and is a separate scope.
The Vitest UI server's /__vitest_attachment__ handler bypasses
isFileServingAllowed via a path-traversal payload, allowing arbitrary file
read/execute on the host. Dev-scope only and not exploitable in
Bambuddy's CI/CLI usage (we don't start the Vitest UI server and
@vitest/ui is not installed), but bumping clears the Dependabot alert
and brings us onto the supported 4.x line.
Bumped:
vitest 3.2.4 → 4.1.8
@vitest/coverage-v8 3.2.4 → 4.1.8
Migration-required fix:
StreamOverlayPage.test.tsx mocked `WebSocket` via
vi.stubGlobal('WebSocket', vi.fn().mockImplementation(() => ({...})))
and the page does `new WebSocket(url)`. Vitest 4 dropped support for
arrow-function constructor mocks ("is not a constructor"). Rewrote
with a plain `function` so `new` resolves correctly.
All 2043 frontend tests pass; npm run build clean; npm audit shows 0
vulnerabilities.
python-multipart 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a
DoS triggered by large preamble/epilogue data around a multipart
boundary. Bambuddy consumes python-multipart transitively through
FastAPI/Starlette for form and file-upload parsing, so multipart routes
(backup restore, project thumbnail upload, etc.) were exposed.
dompurify 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (function-form
ADD_TAGS could bypass FORBID_TAGS). Bambuddy's two call sites use only
array-form ALLOWED_TAGS/ALLOWED_ATTR, so the specific bypass was not
reachable, but the bump still hardens the sanitizer and clears the
audit warning.
requirements.txt floor raised to python-multipart>=0.0.26;
frontend/package.json caret pinned to ^3.4.0; npm audit and pip audit
both report zero outstanding advisories after the bumps.
- Sanitize project notes with DOMPurify before rendering via
dangerouslySetInnerHTML (ProjectDetailPage.tsx)
- Replace hand-rolled HTML sanitizer with DOMPurify in ProjectPageModal
to prevent attribute injection via crafted 3MF href values
- Block /api/v1/auth/setup when auth is already enabled to prevent
unauthenticated clients from disabling authentication remotely
The Raspberry Pi kiosk has no physical keyboard and system-level virtual
keyboards (squeekboard, wvkbd) don't auto-show/hide with labwc/Chromium.
Add a react-simple-keyboard QWERTY keyboard that auto-shows on input
focus, with dark theme, shift/caps/backspace, email keys (@, .), and a
two-phase close that prevents ghost-click passthrough to elements below.
Inputs with data-vkb="false" opt out (e.g. SpoolBuddySettingsPage numpad).
Backend:
- pytest configuration with async support and coverage
- Unit tests for notification service (23 tests)
- Unit tests for smart plug manager (12 tests)
- Unit tests for archive service (16 tests)
- Integration tests for API endpoints
- Fix: notifications now send immediately (digest is summary only)
Frontend:
- Vitest configuration with jsdom and coverage
- MSW for API mocking
- Component tests for Toggle, Button, Card, ConfirmModal (77 tests)
- Test utilities with custom render wrapper
CI/CD:
- GitHub Actions workflow for automated testing
- Backend lint, unit tests, integration tests
- Frontend lint, type-check, unit tests, build