Mantine's Menu mounts Popover + PopoverDropdown + several context providers even
while closed, so a menu per widget was ~13 fibers each for something almost never
opened. WidgetContextMenuProvider now mounts a single menu for the board, anchored
to the cursor; items keep only a contextmenu/long-press listener.
The dropdown takes the section's gridstack ref as a prop rather than reading
SectionContext — the shared menu lives outside any section, and reading it from
context there crashed the board with "useSectionContext must be used within a
SectionContext".
Verified against both builds with scripts/benchmarks/verify-context-menu.mts
(opens on right-click, shows actions, closes on Escape, re-targets to a second
widget):
idle fibers 18,670 -> 16,278 (-12.8%)
fibers after use +4,191 -> +4
JS event listeners 10,128 -> 7,774 (-23%)
DOM nodes 9,189 -> 8,287 (-10%)
browser JS heap 54.1 -> 52.7 MiB
median board load 520 -> 479 ms
container CPU 20.4 -> 20.0 s
Two runs per side, four tabs across two boards. Server memory is unchanged, as
expected for a client-only change. The listener reduction is the point: the 650 MB
session had ~88k listeners and this removes a per-widget source of them.
The gate is validated by passing against unmodified code first. An earlier version
used Playwright's click({ button: "right" }), which emits no contextmenu event
here and so failed shipped code identically — it wrongly condemned a previous
attempt at this optimisation. A gate never run against the un-optimised build
proves only that something changed, not that it still works.
pnpm test 1794/1795 (pre-existing pi-hole Testcontainers flake, 9/9 in isolation);
typecheck clean across 37 packages; 0 lint errors.
@scalar/api-reference-react is a documentation SPA reachable only from one
admin tab, and it renders nothing usable on the server, yet it was compiled
into the server graph for an SSR pass whose output is discarded. It is a
client component already, so next/dynamic with ssr: false is allowed in
place and needs no restructuring. @modelcontextprotocol/sdk is only reachable
through /api/mcp/[transport], so it moves to serverExternalPackages.
Honest accounting: this is an image-size and architecture change, not a
measured memory win. Idle landed at 89.4 MiB against 90.3/91.1/100.5 for the
previous build — the better end of that spread, but inside it, so on one
sample it cannot be claimed as a reduction. What is deterministic is image
size, 401 -> 398 MB. Kept because a docs SPA should not be server-rendered
regardless of what the memory number does.
pnpm typecheck and lint clean; pnpm test 1794/1795 with the known
Testcontainers pi-hole flake, which passes 9/9 in isolation.
- Fix integration data cache: check pending promise before TTL to
prevent returning undefined on concurrent cold-cache access
- Add concurrent-miss test (two parallel callers, one fetch)
- SVG sanitizer: extract <svg> from DOMPurify output to strip
residual HTML wrapper
- Narrow safeRevalidateTag catch: log unexpected errors, silence only
known "outside request scope" messages
- Run oxfmt across all packages (206 files formatted)
Remove incompatible persisted widget query data before resetting a crashed widget so it can fetch fresh data instead of re-rendering the same stale payload.
* fix: context menu refresh stays open with loading spinner, cache age reacts to refetch
* style: auto-fix lint and format
* refactor: pass isFetching as prop to WidgetCacheAge to avoid duplicate useIsFetching
---------
Co-authored-by: ajnart <ajnart@users.noreply.github.com>
* docs(helm): Update Helm chart documentation
* feat(widgets): add right-click context menu with auto-generated toggle actions
- Add WidgetContextMenu component using Mantine 9.3 Menu.ContextMenu
- Extend WidgetDefinition with contextActions API for widget-specific actions
- Auto-generate context menu toggle items for switch-type widget options
- Add 'Search' context action to media-requests widgets (list + stats)
- Keep existing dots-button menu for discoverability in edit mode
- Add widgetStateRef to WidgetComponentProps for client-side-only state
- Add skipContextMenu opt-out on switch option definitions
* style: auto-fix lint and format
* fix(widgets): address CodeRabbit review — badge in context menu target, edit-only pending gate
- Move title badge inside WidgetContextMenu wrapper so right-clicking the title overlay also triggers the context menu
- Remove full-menu hide during integration loading; only disable 'Edit' action while isPending
- Add isPending to allActions useMemo dependency array
* style: auto-fix lint and format
* fix(widgets): gate context menu behind authenticated session
- Only show right-click context menu for logged-in users
- Unauthenticated users see plain children without menu wrapper
- Matches existing BoardItemMenu session gating pattern
* style: auto-fix lint and format
* chore: revert unrelated sitemap change from auto-fix
* style: auto-fix lint and format
* fix(context-menu): persist widget option changes to server
Toggle switches and edit modal changes from the context menu now call
saveBoard mutation to persist to DB. Previously only the client cache
was updated, losing changes on reload. Removes unnecessary invalidation
that caused cascading refetch loops.
* style: auto-fix lint and format
* fix: use explicit React type imports instead of global namespace
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(board): persist category collapse state in localStorage for guests
Authenticated users already had their collapse state saved server-side.
This extends the same behaviour to unauthenticated (guest) users by
reading/writing localStorage keyed by section ID.
On mount, if the user is a guest, the stored value is applied to the
disclosure. On toggle, guests write to localStorage instead of calling
the protected API mutation.
Closes#3355
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: set localStorage value as useDisclosure default instead of useEffect
Replaces the useEffect that read localStorage after mount with a direct
initial value computation passed to useDisclosure. Removes the effect,
the open/close destructuring, and the useEffect import.
Note: initialCollapsed is computed during render rather than post-mount,
which may cause a hydration mismatch on SSR if localStorage differs from
the server-provided section.collapsed. The typeof window guard handles
the SSR case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When adding a widget from the "Choose item to add" modal, the widget
edit modal now opens immediately after placement, allowing the user to
configure options and integrations before the widget renders.
All available integrations for the widget are selected by default.
* fix(deps): update dependency drizzle-zod to ^0.8.2
* chore: update zod to v4 import
* fix: path is no longer available in transform context
* fix: AnyZodObject does no longer exist
* fix: auth env.ts using wrong createEnv and remove unused file env-validation.ts
* fix: required_error no longer exists on z.string
* fix: zod error map is deprecated and replaced with config
* fix: default requires callback now
* fix: migrate zod resolver for mantine
* fix: remove unused form translation file
* fix: wrong enum type
* fix: record now requires two arguments
* fix: add-confirm-password-refinement type issues
* fix: add missing first record argument for entityStateSchema
* fix: migrate superrefine to check
* fix(deps): upgrade zod-form-data to v3
* fix: migrate superRefine to check for mediaUploadSchema
* fix: authProvidersSchema default is array
* fix: use stringbool instead of custom implementation
* fix: record requires first argument
* fix: migrate superRefine to check for certificate router
* fix: confirm pasword refinement is overwriting types
* fix: email optional not working
* fix: migrate intersection to object converter
* fix: safe parse return value rename
* fix: easier access for min and max number value
* fix: migrate superRefine to check for oldmarr import file
* fix: inference of enum shape for old-import board-size wrong
* fix: errors renamed to issues
* chore: address pull request feedback
* fix: zod form requires object
* fix: inference for use-zod-form not working
* fix: remove unnecessary convertion
* fix(deps): upgrade trpc-to-openapi to v3
* fix: build error
* fix: migrate missing zod imports to v4
* fix: migrate zod records to v4
* fix: missing core package dependency in api module
* fix: unable to convert custom zod schema to openapi schema
* fix(deps): upgrade zod to v4
* chore(renovate): enable zod dependency updates
* test: add simple unit test for convertIntersectionToZodObject
---------
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>