Skip to content

feat: add configurable automatic mobile board layout - #6450

Open
ajnart wants to merge 33 commits into
devfrom
feat/auto-mobile-boards
Open

feat: add configurable automatic mobile board layout#6450
ajnart wants to merge 33 commits into
devfrom
feat/auto-mobile-boards

Conversation

@ajnart

@ajnart ajnart commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

  • add a server-wide automatic mobile layout toggle that defaults on for new installs and off for upgrades with saved responsive layouts
  • render phones and small tablets as a read-only two-column board in desktop visual order, with section headings, compact widget summaries, details, actions, and deferred loading
  • retain saved responsive layouts and mobile editing when automatic mode is disabled
  • document both modes and their upgrade/default behavior

Validation

  • 12 affected-package typechecks
  • 17 focused test files, 158 tests
  • production Next.js and documentation builds
  • production-container E2E at 320px, 390px, 430px, phone landscape, 768px tablet, and desktop, including legacy-mode fallback

Summary by CodeRabbit

  • New Features

    • Added an automatic mobile board layout that displays widgets in a read-only, two-column grid.
    • Added a setting to enable or disable automatic mobile layouts.
    • Added support for saved responsive layouts when automatic mobile layout is disabled.
    • Improved desktop and responsive layout editing, including breakpoint validation and clearer controls.
    • Added device-specific mobile home board selection.
  • Bug Fixes

    • Improved widget placement and sizing when adding, moving, or resizing items across layouts.
    • Preserved existing responsive layouts when updating desktop settings.
  • Documentation

    • Updated board, settings, and automation documentation with mobile layout guidance and clearer instructions.

@ajnart
ajnart requested a review from a team as a code owner July 22, 2026 23:54
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds an automatic two-column mobile board, separates desktop and responsive layout editing, makes layout mutations layout-aware, adds mobile-layout settings and migration behavior, and updates related tests, translations, documentation, and CI configuration.

Changes

Automatic mobile board

Layer / File(s) Summary
Layout contracts and settings
packages/validation/src/board.ts, packages/boards/src/context.tsx, apps/nextjs/src/app/.../settings/*, packages/server-settings/*
Adds single-layout validation, responsive-layout constraints, automatic mobile settings, desktop layout selection, and read-only edit-mode support.
Mobile board runtime
apps/nextjs/src/components/board/mobile/*, apps/nextjs/src/components/board/use-mobile-board.tsx, apps/nextjs/src/app/.../boards/*, e2e/mobile-board.spec.ts
Detects mobile viewports, flattens board sections into a two-column grid, hides editing controls, and validates automatic versus legacy mobile rendering.
Layout-aware editing and placement
apps/nextjs/src/components/board/items/actions/*, apps/nextjs/src/components/board/sections/*, packages/api/src/router/board.ts
Passes explicit layout IDs through editing actions, clamps widget dimensions to layout widths, and persists item, layout, and integration rows transactionally.
Server-setting defaults and persistence
packages/db/queries/server-setting.ts, packages/db/migrations/seed.ts, packages/api/src/router/serverSettings.ts, packages/settings/src/creator.ts
Adds automatic-layout defaults and upgrade detection, backfills legacy settings, merges concurrent updates, and supports device-specific home-board selection.
Documentation and repository validation
apps/docs/docs/management/*, packages/translation/src/lang/en.json, .github/workflows/ci.yml, packages/db/test/schema.spec.ts
Documents automatic mobile and responsive layouts, updates translations and schema tests, adds the boards workspace dependency, and changes CI concurrency.

Sequence Diagram(s)

sequenceDiagram
  participant Request as Request headers
  participant Viewport as MobileBoardViewportProvider
  participant Client as ClientBoard
  participant Mobile as MobileBoard
  participant Items as BoardItemContent
  Request->>Viewport: initialize mobile state
  Viewport->>Client: provide viewport and settings state
  Client->>Mobile: render mobile board
  Mobile->>Items: render flattened grid items
Loading

Possibly related PRs

Suggested reviewers: mrjoshuap

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding a configurable automatic mobile board layout.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Yagni / Over-Engineering ✅ Passed The new helpers and tests stay narrowly scoped to the requested mobile-layout feature; I found no extra abstractions or dependencies beyond what it needs.
Docs Are Up To Date ✅ Passed apps/docs was updated in boards, settings, and API docs to cover automatic mobile layout, mobile home board, and related board defaults.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auto-mobile-boards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/api/src/router/board.ts (1)

685-685: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose saveLayout as an MCP tool. This router file requires new useful procedures to be published as MCP tools, and saveLayout is a user-facing mutation. Add a .meta({ mcp: { enabled: true, description: "..." } }) before .input(...).

Suggested change
-  saveLayout: protectedProcedure.input(boardSaveLayoutSchema).mutation(async ({ ctx, input }) => {
+  saveLayout: protectedProcedure
+    .meta({
+      mcp: {
+        enabled: true,
+        description:
+          "Set the desktop column count for a board's layout (recomputes item/section positions). Requires modify permission. REQUIRED: id (board ID from board_getAllBoards), columnCount (1-24)",
+      },
+    })
+    .input(boardSaveLayoutSchema)
+    .mutation(async ({ ctx, input }) => {

As per coding guidelines: "Expose every new useful tRPC procedure as an MCP tool by adding .meta(...)" and "Place the .meta() call before .input()".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/router/board.ts` at line 685, Expose the saveLayout
procedure as an MCP tool by adding a .meta configuration with mcp.enabled set to
true and a clear description before .input(boardSaveLayoutSchema). Keep the
existing protectedProcedure mutation behavior unchanged.

Source: Coding guidelines

apps/nextjs/src/components/board/mobile/mobile-layout.ts (1)

1-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mobile column count ("2") is duplicated between the layout generator and the CSS module. Both files encode the same 2-column constraint independently; changing one without the other would silently mis-size mobile items relative to the actual grid.

  • apps/nextjs/src/components/board/mobile/mobile-layout.ts#L1-L106: mobileColumnCount = 2 clamps item widths for the mobile grid.
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css#L1-L20: grid-template-columns: repeat(2, minmax(0, 1fr)); hardcodes the same column count independently.

Consider exporting mobileColumnCount and consuming it in the CSS via a CSS custom property (e.g., set style={{ "--mobile-column-count": mobileColumnCount }} on the grid wrapper and reference repeat(var(--mobile-column-count), minmax(0, 1fr))), or at minimum add a comment in each file cross-referencing the other so future changes stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-layout.ts` around lines 1 -
106, The mobile column count is duplicated between the layout generator and CSS
grid. Update apps/nextjs/src/components/board/mobile/mobile-layout.ts:1-106 to
expose and reuse mobileColumnCount as the single source of truth, update the
grid wrapper and
apps/nextjs/src/components/board/mobile/mobile-board.module.css:1-20 to consume
that value through a CSS custom property, and replace the hardcoded repeat(2,
...) count; keep item width clamping aligned with the grid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/nextjs/src/components/board/mobile/mobile-board.module.css`:
- Line 16: Update the Stylelint configuration’s selector-pseudo-class-no-unknown
rule to ignore the CSS Modules :global pseudo-class, preserving existing
validation for other unknown pseudo-classes. Check the existing configuration
and reuse its current rule structure rather than adding a separate override.

In `@apps/nextjs/src/components/board/mobile/mobile-board.tsx`:
- Around line 16-25: Update MobileBoard so it does not return null when
rootSection is absent; allow the existing createMobileBoardItems result to
render for boards containing only dynamic sections, while preserving any
rootSection-dependent behavior through an optional or conditional path.

In `@apps/nextjs/src/components/board/use-mobile-board.ts`:
- Around line 5-7: Update useIsMobileBoard to accept and forward a server-known
device or layout hint as Mantine’s initialValue, and configure
getInitialValueInEffect appropriately so mobile clients do not first render the
desktop board. Preserve the existing mobileBoardMediaQuery and allow the current
behavior when no hint is available.

In `@packages/api/src/router/board.ts`:
- Around line 711-750: Update saveLayout to wrap the item-layout updates,
section-layout updates, base-layout update, and legacy-layout deletion in a
single handleTransactionsAsync transaction, reusing the transaction callback
pattern from saveBoard. Ensure every database operation in this recompute flow
uses the transaction context so failures roll back the entire layout change
atomically.

---

Nitpick comments:
In `@apps/nextjs/src/components/board/mobile/mobile-layout.ts`:
- Around line 1-106: The mobile column count is duplicated between the layout
generator and CSS grid. Update
apps/nextjs/src/components/board/mobile/mobile-layout.ts:1-106 to expose and
reuse mobileColumnCount as the single source of truth, update the grid wrapper
and apps/nextjs/src/components/board/mobile/mobile-board.module.css:1-20 to
consume that value through a CSS custom property, and replace the hardcoded
repeat(2, ...) count; keep item width clamping aligned with the grid.

In `@packages/api/src/router/board.ts`:
- Line 685: Expose the saveLayout procedure as an MCP tool by adding a .meta
configuration with mcp.enabled set to true and a clear description before
.input(boardSaveLayoutSchema). Keep the existing protectedProcedure mutation
behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 85878807-d247-4d89-aa4d-5b9049e55931

📥 Commits

Reviewing files that changed from the base of the PR and between bfba3c9 and 535a8fc.

📒 Files selected for processing (15)
  • apps/docs/docs/management/boards/index.mdx
  • apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/use-mobile-board.ts
  • packages/api/src/router/board.ts
  • packages/api/src/router/test/board.spec.ts
  • packages/boards/src/context.tsx
  • packages/boards/src/edit-mode.tsx
  • packages/translation/src/lang/en.json
  • packages/validation/src/board.ts

Comment thread apps/nextjs/src/components/board/mobile/mobile-board.module.css
Comment thread apps/nextjs/src/components/board/mobile/mobile-board.tsx Outdated
Comment thread apps/nextjs/src/components/board/use-mobile-board.ts Outdated
Comment thread packages/api/src/router/board.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/nextjs/src/components/board/mobile/mobile-board.tsx`:
- Around line 20-25: Update the rootSection selection to use the same ordering
as createMobileBoardItems: filter out dynamic sections, sort the remaining
sections by yOffset and then xOffset, and select the first one. Preserve the
existing mobile fallback object when no non-dynamic sections exist, and pass
this consistently to SectionProvider.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ad79b19f-1879-4448-9b33-c5b191beecd3

📥 Commits

Reviewing files that changed from the base of the PR and between 535a8fc and 612f2d7.

📒 Files selected for processing (6)
  • apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/use-mobile-board.tsx
  • packages/api/src/router/board.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • packages/api/src/router/board.ts

Comment thread apps/nextjs/src/components/board/mobile/mobile-board.tsx Outdated
@ajnart ajnart added the needs-demo This PR needs a demo deployment label Jul 23, 2026
@dokploy-homarr-labs

dokploy-homarr-labs Bot commented Jul 23, 2026

Copy link
Copy Markdown

Dokploy Preview Deployment

Name Status Preview Updated (UTC)
homarr ✅ Done Preview URL 2026-07-29T19:21:27.620Z

@ajnart
ajnart force-pushed the feat/auto-mobile-boards branch from 3f3ba61 to 7bcfdd6 Compare July 23, 2026 07:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx (1)

56-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

!isMobile / hidden={isMobile} are dead code after the early mobile return.

Since BoardContentHeaderActions already returns <MobileMoreMenu/> when isMobile is true (L63-65), isMobile is guaranteed false for the rest of the render. That makes isEditMode && !isMobile && <AddMenu/> (L73) equivalent to just isEditMode, and hidden={isMobile} on <EditModeMenu/> (L75) always resolves to hidden={false} — the hidden prop's actual guard logic in EditModeMenu (L207-219) is unreachable from this call site. This is confusing for anyone reading the code without tracing the control flow.

♻️ Suggested simplification (if `EditModeMenu`'s `hidden` isn't needed elsewhere)
-      {isEditMode && !isMobile && <AddMenu />}
+      {isEditMode && <AddMenu />}

-      <EditModeMenu demoReadOnly={demoReadOnly} hidden={isMobile} />
+      <EditModeMenu demoReadOnly={demoReadOnly} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx around
lines 56 - 75, Remove the redundant !isMobile condition from the AddMenu render
and remove the always-false hidden={isMobile} prop from EditModeMenu in
BoardContentHeaderActions, since the earlier mobile return guarantees the
remaining branch is non-mobile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx:
- Around line 341-345: Update the sign-out handler around signOut in the onClick
callback to handle rejected promises with a catch path. Ensure failures do not
produce an unhandled rejection and still perform the logoutUrl-or-/auth/login
redirect, while preserving the existing redirect behavior on successful
sign-out.

---

Nitpick comments:
In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx:
- Around line 56-75: Remove the redundant !isMobile condition from the AddMenu
render and remove the always-false hidden={isMobile} prop from EditModeMenu in
BoardContentHeaderActions, since the earlier mobile return guarantees the
remaining branch is non-mobile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 14180e4c-2e4a-42ad-b69d-89ded02659de

📥 Commits

Reviewing files that changed from the base of the PR and between 54d797e and 7bcfdd6.

📒 Files selected for processing (46)
  • apps/docs/docs/management/boards/index.mdx
  • apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
  • apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
  • apps/nextjs/src/app/[locale]/boards/layout.tsx
  • apps/nextjs/src/components/board/items/item-content.tsx
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.ts
  • apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx
  • apps/nextjs/src/components/board/use-mobile-board.spec.ts
  • apps/nextjs/src/components/board/use-mobile-board.tsx
  • apps/nextjs/src/components/layout/header.tsx
  • apps/nextjs/src/components/layout/header/button.tsx
  • apps/nextjs/src/components/layout/header/responsive-user.tsx
  • apps/nextjs/src/components/layout/header/search.tsx
  • apps/nextjs/src/components/layout/header/user.tsx
  • apps/nextjs/src/components/layout/shell.tsx
  • packages/translation/src/lang/en.json
  • packages/widgets/src/app/index.ts
  • packages/widgets/src/calendar/component.tsx
  • packages/widgets/src/calendar/index.ts
  • packages/widgets/src/clock/index.ts
  • packages/widgets/src/common/mobile-summary.tsx
  • packages/widgets/src/definition.ts
  • packages/widgets/src/downloads/component.tsx
  • packages/widgets/src/downloads/index.ts
  • packages/widgets/src/index.tsx
  • packages/widgets/src/media-missing/component.tsx
  • packages/widgets/src/media-missing/index.ts
  • packages/widgets/src/media-releases/component.tsx
  • packages/widgets/src/media-releases/index.ts
  • packages/widgets/src/media-requests/list/component.tsx
  • packages/widgets/src/media-requests/list/index.ts
  • packages/widgets/src/media-transcoding/component.tsx
  • packages/widgets/src/media-transcoding/index.ts
  • packages/widgets/src/notifications/component.tsx
  • packages/widgets/src/notifications/index.ts
  • packages/widgets/src/smart-home/entity-state/index.ts
  • packages/widgets/src/smart-home/execute-automation/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
  • apps/docs/docs/management/boards/index.mdx

Comment thread apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx Outdated
@ajnart

ajnart commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread packages/widgets/src/notebook/component.tsx Fixed
@ajnart

ajnart commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Nerdy-Technician

Copy link
Copy Markdown
Contributor

Let me know when U want me to test 🙂

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts (1)

29-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test passes vacuously.

root.querySelectorAll(...) never returns root itself, so the assertion holds even if the dialog !== currentDialog exclusion were deleted. Nest the details dialog inside a shared root (with a sibling non-dialog) so the exclusion logic is actually exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts` around
lines 29 - 34, Update the test “does not treat the current details dialog as
nested” to render the details dialog inside a shared root alongside a non-dialog
sibling, then call hasOpenDialogOutside with that root and details dialog. Keep
the assertion false so the test specifically exercises exclusion of the current
dialog rather than relying on querySelectorAll omitting the root.
packages/widgets/src/system-resources/component.tsx (1)

88-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider labelling the direction arrows for assistive tech.

/ carry the only download/upload semantics here and screen readers announce them inconsistently (or as "down arrow"). A localized prefix (e.g. t("card.network.down")) or a visually-hidden label would make the mobile summary self-describing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widgets/src/system-resources/component.tsx` around lines 88 - 92,
Update the network value and description in the component’s system-resources
card to provide localized download/upload text for assistive technologies
instead of relying solely on the ↓ and ↑ symbols. Reuse the existing translation
function t with appropriate network direction keys, while preserving the current
formatted rates and fallback behavior.
apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts (1)

35-43: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Observer fires a document-wide query on every subtree mutation.

subtree: true + childList: true on document.body means any DOM churn inside the open details widget (charts, lists, live data) triggers a full document.querySelectorAll('[role="dialog"]'). Scoping childList observation or batching update in a microtask/rAF would keep this off the mutation hot path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts` around lines
35 - 43, Reduce the mutation overhead in the observer setup around the mobile
dialog stack’s `MutationObserver` by batching `update` calls with a microtask or
animation frame, or by narrowing child-list observation to the relevant dialog
container. Preserve updates for dialog additions, removals, and relevant
attribute changes while preventing every document-wide subtree mutation from
triggering an immediate `document.querySelectorAll('[role="dialog"]')`.
apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx (1)

24-67: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Solid deferred-mount/unmount logic; consider debouncing the per-item resize listener.

The eager / mount-once / continuous-unmount branches are all correctly guarded. One nit: every mounted DeferredMobileItem registers its own resize listener that re-runs observe() (potentially recreating the IntersectionObserver) on every resize tick. On a board with many widgets, rapid resize events (e.g. orientation change) could trigger many synchronous re-observations at once. Debouncing observe (or coordinating recalculation from a single parent listener) would reduce this cost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx` around
lines 24 - 67, Debounce the per-item resize handling in the useEffect hook so
rapid resize events do not synchronously recreate the IntersectionObserver
through observe. Preserve the existing eager, mount-once, offscreen-unmount,
cleanup, and root-margin recalculation behavior, including cancelling any
pending debounce during effect cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/docs/management/boards/index.mdx`:
- Line 43: In the public-board description, update the misspelled word “fiends”
to “friends” while preserving the rest of the sentence.

In `@apps/nextjs/src/components/board/mobile/mobile-board.module.css`:
- Around line 9-19: Replace the deprecated clip declaration in .boardTitle with
the equivalent clip-path declaration, preserving the existing visually-hidden
behavior and all other accessibility styles unchanged.

In `@packages/widgets/src/beszel/_shared/mobile-summary.tsx`:
- Around line 21-29: In packages/widgets/src/beszel/_shared/mobile-summary.tsx
lines 21-29, update the error throw in the mobile summary query flow to occur
only when results.length === 0; retain failed-result handling and render cached
results with isStale when usable results exist. In
packages/widgets/src/beszel-alerts/component.tsx lines 73-78, likewise gate the
alertsError throw on alerts results being empty so cached alerts continue
rendering during failed background refetches.

In `@packages/widgets/src/notebook/component.tsx`:
- Around line 19-20: Update the HTML-stripping replacements in the notebook
component to allow optional whitespace between the closing script/style tag
names and the closing angle bracket, while preserving case-insensitive matching
and removal of the entire element contents.

In `@packages/widgets/src/system-disks/component.tsx`:
- Around line 160-161: Update the temperature computation in the system disk
component to use a nullish availability check for smartItem?.temperature, so a
valid 0°C value is formatted and displayed while null or undefined remains
omitted.

In `@packages/widgets/src/weather/component.tsx`:
- Around line 59-63: Update getPreferredUnit usage in the current-weather,
weekly current-weather, and forecast-day accessible labels so a temperature
value of 0 is passed through rather than treated as missing; apply the same
zero-preserving change at all three listed sites in
packages/widgets/src/weather/component.tsx.

---

Nitpick comments:
In `@apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx`:
- Around line 24-67: Debounce the per-item resize handling in the useEffect hook
so rapid resize events do not synchronously recreate the IntersectionObserver
through observe. Preserve the existing eager, mount-once, offscreen-unmount,
cleanup, and root-margin recalculation behavior, including cancelling any
pending debounce during effect cleanup.

In `@apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts`:
- Around line 29-34: Update the test “does not treat the current details dialog
as nested” to render the details dialog inside a shared root alongside a
non-dialog sibling, then call hasOpenDialogOutside with that root and details
dialog. Keep the assertion false so the test specifically exercises exclusion of
the current dialog rather than relying on querySelectorAll omitting the root.

In `@apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts`:
- Around line 35-43: Reduce the mutation overhead in the observer setup around
the mobile dialog stack’s `MutationObserver` by batching `update` calls with a
microtask or animation frame, or by narrowing child-list observation to the
relevant dialog container. Preserve updates for dialog additions, removals, and
relevant attribute changes while preventing every document-wide subtree mutation
from triggering an immediate `document.querySelectorAll('[role="dialog"]')`.

In `@packages/widgets/src/system-resources/component.tsx`:
- Around line 88-92: Update the network value and description in the component’s
system-resources card to provide localized download/upload text for assistive
technologies instead of relying solely on the ↓ and ↑ symbols. Reuse the
existing translation function t with appropriate network direction keys, while
preserving the current formatted rates and fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23e23d9d-aa82-4a8a-8d99-1cf06fde046c

📥 Commits

Reviewing files that changed from the base of the PR and between 7bcfdd6 and e1399d4.

📒 Files selected for processing (116)
  • apps/docs/docs/management/boards/index.mdx
  • apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.spec.ts
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.ts
  • apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
  • apps/nextjs/src/components/board/items/item-content.tsx
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.spec.ts
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.ts
  • apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx
  • apps/nextjs/src/components/layout/header/search.tsx
  • e2e/mobile-board.spec.ts
  • packages/api/src/router/board.ts
  • packages/api/src/router/test/board.spec.ts
  • packages/api/src/router/widgets/calendar.ts
  • packages/api/src/router/widgets/downloads.ts
  • packages/api/src/router/widgets/health-monitoring.ts
  • packages/api/src/router/widgets/media-release.ts
  • packages/api/src/router/widgets/media-requests.ts
  • packages/api/src/router/widgets/media-server.ts
  • packages/api/src/router/widgets/notifications.ts
  • packages/api/src/router/widgets/uptime-kuma.ts
  • packages/api/src/settle-integrations.spec.ts
  • packages/api/src/settle-integrations.ts
  • packages/translation/src/lang/en.json
  • packages/widgets/src/anchor-note/index.ts
  • packages/widgets/src/app/component.tsx
  • packages/widgets/src/archive-team-warrior/index.ts
  • packages/widgets/src/audio-stats/index.ts
  • packages/widgets/src/bazarr/index.ts
  • packages/widgets/src/beszel-alerts/component.tsx
  • packages/widgets/src/beszel-alerts/index.ts
  • packages/widgets/src/beszel-system-grid/component.tsx
  • packages/widgets/src/beszel-system-grid/index.ts
  • packages/widgets/src/beszel-system-stats/component.tsx
  • packages/widgets/src/beszel-system-stats/index.ts
  • packages/widgets/src/beszel-system-table/component.tsx
  • packages/widgets/src/beszel-system-table/index.ts
  • packages/widgets/src/beszel/_shared/mobile-summary.tsx
  • packages/widgets/src/bookmarks/index.tsx
  • packages/widgets/src/calendar/component.tsx
  • packages/widgets/src/calendar/index.ts
  • packages/widgets/src/common/mobile-summary.tsx
  • packages/widgets/src/coolify/applications-section.tsx
  • packages/widgets/src/coolify/component.tsx
  • packages/widgets/src/coolify/index.ts
  • packages/widgets/src/coolify/instance-card.tsx
  • packages/widgets/src/coolify/resource-row.tsx
  • packages/widgets/src/coolify/servers-section.tsx
  • packages/widgets/src/coolify/services-section.tsx
  • packages/widgets/src/coolify/single-instance-layout.tsx
  • packages/widgets/src/custom-api/component.tsx
  • packages/widgets/src/custom-api/index.ts
  • packages/widgets/src/custom-api/jsx-interactive-components.tsx
  • packages/widgets/src/definition.ts
  • packages/widgets/src/dns-hole/controls/TimerModal.tsx
  • packages/widgets/src/dns-hole/controls/component.tsx
  • packages/widgets/src/dns-hole/controls/index.ts
  • packages/widgets/src/docker/component.tsx
  • packages/widgets/src/docker/index.ts
  • packages/widgets/src/downloads/component.tsx
  • packages/widgets/src/downloads/index.ts
  • packages/widgets/src/firewall/index.ts
  • packages/widgets/src/health-monitoring/component.tsx
  • packages/widgets/src/health-monitoring/index.ts
  • packages/widgets/src/health-monitoring/system-health.tsx
  • packages/widgets/src/iframe/component.tsx
  • packages/widgets/src/iframe/index.ts
  • packages/widgets/src/indexer-manager/index.ts
  • packages/widgets/src/media-missing/component.tsx
  • packages/widgets/src/media-missing/index.ts
  • packages/widgets/src/media-releases/component.tsx
  • packages/widgets/src/media-requests/list/component.tsx
  • packages/widgets/src/media-requests/list/index.ts
  • packages/widgets/src/media-requests/stats/index.ts
  • packages/widgets/src/media-server/component.tsx
  • packages/widgets/src/media-server/index.ts
  • packages/widgets/src/media-transcoding/component.tsx
  • packages/widgets/src/media-transcoding/index.ts
  • packages/widgets/src/notebook/component.tsx
  • packages/widgets/src/notebook/index.ts
  • packages/widgets/src/notebook/notebook.tsx
  • packages/widgets/src/notifications/component.tsx
  • packages/widgets/src/notifications/index.ts
  • packages/widgets/src/paperless-ngx/index.ts
  • packages/widgets/src/patchmon/index.ts
  • packages/widgets/src/releases/index.ts
  • packages/widgets/src/rssFeed/component.tsx
  • packages/widgets/src/rssFeed/index.ts
  • packages/widgets/src/smart-home/entity-state/index.ts
  • packages/widgets/src/speedtest-tracker/index.ts
  • packages/widgets/src/stocks/index.ts
  • packages/widgets/src/system-disks/component.tsx
  • packages/widgets/src/system-disks/index.ts
  • packages/widgets/src/system-resources/component.tsx
  • packages/widgets/src/system-resources/index.ts
  • packages/widgets/src/timetable/index.ts
  • packages/widgets/src/tracearr/index.ts
  • packages/widgets/src/traefik/index.ts
  • packages/widgets/src/umami/index.ts
  • packages/widgets/src/ups/index.ts
  • packages/widgets/src/uptime-kuma/component.tsx
  • packages/widgets/src/uptime-kuma/index.ts
  • packages/widgets/src/vpn/index.ts
  • packages/widgets/src/weather/component.tsx
  • packages/widgets/src/weather/index.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
  • packages/widgets/src/media-transcoding/index.ts
  • packages/widgets/src/calendar/index.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • packages/widgets/src/media-requests/list/index.ts
  • apps/nextjs/src/components/layout/header/search.tsx
  • packages/widgets/src/downloads/index.ts
  • packages/widgets/src/notifications/index.ts
  • packages/widgets/src/media-releases/component.tsx
  • packages/widgets/src/smart-home/entity-state/index.ts
  • packages/widgets/src/media-requests/list/component.tsx
  • packages/widgets/src/definition.ts
  • packages/widgets/src/media-missing/index.ts
  • packages/widgets/src/media-missing/component.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • packages/widgets/src/calendar/component.tsx
  • packages/widgets/src/notifications/component.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx

Comment thread apps/docs/docs/management/boards/index.mdx
Comment thread apps/nextjs/src/components/board/mobile/mobile-board.module.css Outdated
Comment thread packages/widgets/src/beszel/_shared/mobile-summary.tsx Outdated
Comment thread packages/widgets/src/notebook/component.tsx Outdated
Comment thread packages/widgets/src/system-disks/component.tsx Outdated
Comment thread packages/widgets/src/weather/component.tsx Outdated
@ajnart

ajnart commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ajnart
ajnart force-pushed the feat/auto-mobile-boards branch from 890a9c8 to 7959c20 Compare July 28, 2026 17:40
@ajnart ajnart changed the title feat: add automatic mobile board layout feat: add configurable automatic mobile board layout Jul 28, 2026
@ajnart

ajnart commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (13)
apps/nextjs/src/components/board/use-mobile-board.spec.ts (1)

77-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the skeleton test into its own describe and assert the positive branch. It currently sits under resolveIsAutomaticMobileBoard, and only the false case is covered — the automatic + unresolved case that actually gates the skeleton render is untested.

♻️ Proposed refactor
-  test("does not show the tablet resolution skeleton in legacy mode", () => {
-    expect(
-      shouldShowMobileBoardViewportSkeleton({
-        enableAutomaticMobileLayout: false,
-        isResolved: false,
-      }),
-    ).toBe(false);
-  });
-});
+});
+
+describe("shouldShowMobileBoardViewportSkeleton", () => {
+  test("does not show the tablet resolution skeleton in legacy mode", () => {
+    expect(shouldShowMobileBoardViewportSkeleton({ enableAutomaticMobileLayout: false, isResolved: false })).toBe(
+      false,
+    );
+  });
+
+  test("shows the skeleton while an automatic mobile viewport is unresolved", () => {
+    expect(shouldShowMobileBoardViewportSkeleton({ enableAutomaticMobileLayout: true, isResolved: false })).toBe(true);
+  });
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/use-mobile-board.spec.ts` around lines 77 -
85, Move the shouldShowMobileBoardViewportSkeleton test out of the
resolveIsAutomaticMobileBoard describe block into a dedicated describe. Add
coverage for enableAutomaticMobileLayout true with isResolved false and assert
that the skeleton is shown, while preserving the existing legacy-mode assertion.
apps/nextjs/src/components/board/mobile/mobile-presentation.ts (1)

18-19: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Gate unmountWhenOffscreen on the resolved eager value, not just the explicit flag. When eager comes from the app/bookmarks default, an explicit unmountWhenOffscreen: true still wins, producing an eagerly mounted item that unmounts on scroll.

♻️ Proposed refactor
-  return {
+  const eager = mobile?.eager ?? (item.kind === "app" || item.kind === "bookmarks");
+
+  return {
     width: mobile?.width ?? (item.kind === "app" ? 1 : 2),
     height: mobile?.height ?? (item.kind === "app" || usesGenericSummary ? 1 : Math.max(1, Math.min(item.height, 3))),
     displayMode: supportsCompactSummary || usesGenericSummary ? ("mobileSummary" as const) : ("default" as const),
     supportsDetails,
     usesGenericSummary,
-    eager: mobile?.eager ?? (item.kind === "app" || item.kind === "bookmarks"),
-    unmountWhenOffscreen: mobile?.eager === true ? false : (mobile?.unmountWhenOffscreen ?? false),
+    eager,
+    unmountWhenOffscreen: eager ? false : (mobile?.unmountWhenOffscreen ?? false),
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-presentation.ts` around lines
18 - 19, Update the mobile presentation options so unmountWhenOffscreen is
disabled whenever the resolved eager value is true, including when eager uses
the app/bookmarks default. Base the condition on the resolved eager expression
or value rather than only mobile?.eager === true, while preserving the existing
fallback behavior for non-eager items.
apps/nextjs/src/components/board/mobile/mobile-viewport-observer.spec.ts (1)

39-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider resetting the observer module between tests. mobile-viewport-observer keeps observer/subscriptions/resize-listener state at module scope, so these tests only pass because each one happens to fully unsubscribe. Adding vi.resetModules() with a dynamic import (or an exported reset for tests) would make the suite robust to future tests that leave a subscription open.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-viewport-observer.spec.ts`
around lines 39 - 47, Reset the module-scoped observer state between tests in
the mobile viewport observer spec by using vi.resetModules() and dynamically
importing mobile-viewport-observer for each test, or by invoking an exported
test reset hook. Ensure observer, subscriptions, and resize-listener state is
freshly initialized before every test while preserving the existing
IntersectionObserverMock setup.
packages/boards/src/context.spec.ts (1)

16-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the undefined-viewport and no-layout paths.

getCurrentLayout(board, false, undefined) (SSR path, falls back to desktop) and the Board must have a layout throw are both untested branches of the new logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/boards/src/context.spec.ts` around lines 16 - 36, Extend the “board
layout selection” tests to cover getCurrentLayout with legacy mode and an
undefined viewport, asserting it falls back to the desktop “large” layout. Also
add a test for getCurrentLayout when the board has no layouts, asserting it
throws the “Board must have a layout” error.
apps/nextjs/src/components/board/use-mobile-board.tsx (1)

76-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

hasResolvedClientWidth is really a "has mounted" flag.

It is set unconditionally on mount and never reflects width resolution; renaming it (e.g. hasMounted) would make the tablet gate in resolveMobileBoardViewport easier to reason about.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/use-mobile-board.tsx` around lines 76 - 89,
Rename hasResolvedClientWidth and its setter to hasMounted and setHasMounted
throughout the component, including the useEffect and resolveMobileBoardViewport
call. Preserve the existing mount-only initialization and pass the renamed flag
to the viewport resolver.
apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx (1)

67-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

isMobile gating below the early return is dead code.

Line 67 already returns for mobile, so !isMobile on line 77 is always true and hidden={isMobile} on line 79 is always false — which also makes the entire hidden branch in EditModeMenu (lines 212, 221) unreachable. Either drop the hidden prop or keep EditModeMenu rendered on mobile if hotkey suppression was the goal.

♻️ Suggested simplification
-      {isEditMode && !isMobile && <AddMenu />}
+      {isEditMode && <AddMenu />}
 
-      <EditModeMenu demoReadOnly={demoReadOnly} hidden={isMobile} />
+      <EditModeMenu demoReadOnly={demoReadOnly} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx around
lines 67 - 79, Remove the dead mobile gating from the desktop-only render path
in the header actions component: update the isEditMode AddMenu condition to omit
!isMobile and remove the always-false hidden={isMobile} prop from EditModeMenu.
Keep the existing early mobile return and its MobileMoreMenu behavior unchanged.
apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx (1)

27-57: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Keep the viewport callback out of the observer dependencies.

MobileBoardItem passes the raw state setter (setIsNearViewport) as onNearViewportChange; each render changes state and recreates the IntersectionObserver. Use a ref-backed callback and exclude it from the effect dependencies so the observer lifecycle depends only on eager and unmountWhenOffscreen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx` around
lines 27 - 57, The useEffect in DeferredMobileItem recreates its viewport
observer because onNearViewportChange changes identity on each render. Store the
latest callback in a ref-backed stable handler, invoke that handler from the
observer callback, and remove onNearViewportChange from the effect dependency
array so the observer lifecycle depends only on eager and unmountWhenOffscreen.
packages/db/queries/server-setting.ts (2)

46-75: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant double-parse of the persisted "board" setting.

persistedSettings is already parsed generically for "board" inside the reduce (line 57), then persistedBoardSettings.value is parsed again separately (line 67) just to type-check enableAutomaticMobileLayout. Capturing the parsed board value once (e.g., from within the reduce, or hoisting the board lookup) would avoid parsing the same JSON string twice per call.

♻️ Example consolidation
-  const mergedSettings = defaultServerSettingsKeys.reduce((acc, settingKey) => {
-    const setting = settings.find((candidate) => candidate.settingKey === settingKey);
-    if (!setting) {
-      // Typescript is not happy because the key is a union and it does not know that they are the same
-      acc[settingKey] = { ...defaultServerSettings[settingKey] } as never;
-      return acc;
-    }
-
-    const persistedSettings = parse<Record<string, unknown>>(setting.value);
-    acc[settingKey] = {
-      ...defaultServerSettings[settingKey],
-      ...persistedSettings,
-    } as never;
-    return acc;
-  }, {} as ServerSettings);
-
-  const persistedBoardSettings = settings.find((setting) => setting.settingKey === "board");
-  const parsedBoardSettings = persistedBoardSettings
-    ? parse<Partial<ServerSettings["board"]>>(persistedBoardSettings.value)
-    : {};
-
-  if (typeof parsedBoardSettings.enableAutomaticMobileLayout !== "boolean") {
+  let parsedBoardSettings: Partial<ServerSettings["board"]> = {};
+  const mergedSettings = defaultServerSettingsKeys.reduce((acc, settingKey) => {
+    const setting = settings.find((candidate) => candidate.settingKey === settingKey);
+    if (!setting) {
+      acc[settingKey] = { ...defaultServerSettings[settingKey] } as never;
+      return acc;
+    }
+
+    const persistedSettings = parse<Record<string, unknown>>(setting.value);
+    if (settingKey === "board") parsedBoardSettings = persistedSettings as Partial<ServerSettings["board"]>;
+    acc[settingKey] = {
+      ...defaultServerSettings[settingKey],
+      ...persistedSettings,
+    } as never;
+    return acc;
+  }, {} as ServerSettings);
+
+  if (typeof parsedBoardSettings.enableAutomaticMobileLayout !== "boolean") {
     mergedSettings.board.enableAutomaticMobileLayout = await getAutomaticMobileLayoutUpgradeDefaultAsync(db);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/queries/server-setting.ts` around lines 46 - 75, Update
getServerSettingsAsync to reuse the already parsed board settings from the
defaultServerSettingsKeys reduce when checking enableAutomaticMobileLayout,
instead of parsing persistedBoardSettings.value again. Preserve the existing
fallback to getAutomaticMobileLayoutUpgradeDefaultAsync when the reused value is
not boolean.

16-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a test for the duplicate-canonical-layout branch.

The nextCount > 1 per-board counter path (defending against duplicate "Base"/breakpoint-0 rows for one board) isn't exercised by any of the provided seed.spec.ts cases — every negative test currently hits the name !== "Base" || breakpoint !== 0 check first. A small added case with two { name: "Base", breakpoint: 0 } rows for the same boardId would close this gap for a heuristic that gates a server-wide rendering mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/queries/server-setting.ts` around lines 16 - 32, The test suite
should cover the duplicate canonical-layout branch in
shouldEnableAutomaticMobileLayoutForUpgrade by adding a case with two Base
layouts at breakpoint 0 sharing the same boardId, and assert that it returns
false.
packages/widgets/src/media-requests/stats/component.tsx (1)

107-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mobile summary description hardcodes word order instead of using an ICU placeholder.

${pending} ${t("titles.stats.pending")} bakes English word order into the component. Other locales can't reorder the number relative to the label, unlike every other interpolated string in en.json (e.g. footer.updated, remaining).

🛠️ Suggested approach
-        description={`${pending} ${t("titles.stats.pending")}`}
+        description={t("titles.stats.pendingCount", { pending })}

Add "pendingCount": "{pending} pending approvals" (or similar) to en.json so translators control word order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/widgets/src/media-requests/stats/component.tsx` around lines 107 -
119, The mobile summary description in the displayMode === "mobileSummary"
branch should use a translated ICU message instead of concatenating pending and
the localized label. Add a pending-count translation key with a {pending}
placeholder to the existing English translations, then pass the pending value
through that key in the WidgetMobileSummary description so locales can control
word order.
packages/api/src/settle-integrations.ts (1)

22-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

getIntegrationQueryProvenance silently undercounts failures if a fallback is combined with it.

failedIntegrationCount is derived from selectedIntegrationCount - results.length. This is accurate today because no caller passes fallback, but settleIntegrationQueries's fallback option backfills failed results into the same array, making results.length === integrations.length even when integrations failed — silently zeroing out failedIntegrationCount for any future caller that combines both options.

🛠️ Suggested doc guard
 export const getIntegrationQueryProvenance = (
   selectedIntegrationCount: number,
   results: readonly { isStale?: boolean }[],
 ): IntegrationQueryProvenance => ({
+  // NOTE: assumes `results` came from settleIntegrationQueries without a `fallback` option.
+  // A `fallback` backfills failed integrations into `results`, which would undercount failures here.
   failedIntegrationCount: Math.max(0, selectedIntegrationCount - results.length),
   staleIntegrationCount: results.filter((result) => result.isStale === true).length,
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/settle-integrations.ts` around lines 22 - 28, Update
getIntegrationQueryProvenance and its callers so failedIntegrationCount is based
on the original failed-query information rather than results.length when
settleIntegrationQueries uses fallback. Preserve staleIntegrationCount from the
returned results, and add a guard in the relevant API or documentation to
prevent combining fallback with provenance calculation unless failure counts
remain available.
e2e/mobile-board.spec.ts (1)

164-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Scenario selection keyed on pixel widths is fragile.

viewport.width === 390 / === 320 silently disables the reduced-motion, zoom-overflow, and interaction/RTL scenarios if a viewport size is ever tweaked. Put explicit flags on the viewport descriptors instead.

♻️ Suggested restructure
-        { name: "small phone", width: 320, height: 568, userAgent: devices["iPhone 13"].userAgent, phone: true },
-        { name: "phone", width: 390, height: 844, userAgent: devices["iPhone 13"].userAgent, phone: true },
+        {
+          name: "small phone",
+          width: 320,
+          height: 568,
+          userAgent: iPhoneUserAgent,
+          phone: true,
+          checksZoomOverflow: true,
+        },
+        {
+          name: "phone",
+          width: 390,
+          height: 844,
+          userAgent: iPhoneUserAgent,
+          phone: true,
+          reducedMotion: true,
+          checksInteractions: true,
+        },

then branch on viewport.checksZoomOverflow / viewport.checksInteractions / viewport.reducedMotion.

Also applies to: 191-191, 237-237, 251-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/mobile-board.spec.ts` around lines 164 - 182, Add explicit scenario flags
to each mobileViewports descriptor for reduced-motion, zoom-overflow, and
interaction/RTL checks, then update the affected scenario branches to use
viewport.reducedMotion, viewport.checksZoomOverflow, and
viewport.checksInteractions instead of hard-coded width comparisons. Preserve
the current scenario assignments for each viewport while removing the fragile
pixel-width coupling.
packages/api/src/router/test/serverSettings.spec.ts (1)

133-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Backfill assertion can't fail for the wrong reason it's guarding.

On an empty DB the upgrade default resolves to the same value as defaultServerSettings.board.enableAutomaticMobileLayout, so this test passes even if the backfill branch in getServerSettingByKeyAsync (packages/db/queries/server-setting.ts:77-108) were removed. Seed a board with a saved responsive layout first so the upgrade default flips, then assert the flipped value — that actually exercises the legacy path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/router/test/serverSettings.spec.ts` around lines 133 - 153,
The test “getBoardSettings should backfill the automatic layout default for
legacy values” currently cannot distinguish the backfill behavior from the
normal default. Seed the database with a saved responsive layout before
inserting the legacy board settings, then assert that getBoardSettings returns
the flipped enableAutomaticMobileLayout value while preserving the other default
board settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/docs/management/api/index.mdx`:
- Around line 83-88: Remove the UI-only saved responsive layouts bullet from the
OpenAPI/MCP endpoint list near the `boards` and `settings` API reference
guidance. Keep the endpoint list limited to API-supported capabilities, and
preserve the note explaining that saved responsive layouts are intentionally not
exposed as OpenAPI or MCP operations.

In `@apps/docs/docs/management/boards/index.mdx`:
- Line 273: Update the sentence describing the global home board to refer to
people as “users who,” replacing “users that” while preserving the meaning and
surrounding wording.
- Line 37: Update the sentence in the board documentation to use the hyphenated
compound adjective “drag-and-drop” instead of “drag and drop,” without changing
the surrounding wording.

In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx:
- Around line 553-580: Update handleClick to bypass leave confirmation and
preserve normal browser behavior for same-document hash-only links matched by
anchorSelector. Detect destinations whose URL has no pathname or search change
and only a hash fragment, returning before preventDefault(); keep confirmation
and navigation handling unchanged for actual page transitions.

In `@packages/api/src/router/serverSettings.ts`:
- Around line 105-119: The saveSettings mutation currently uses a non-atomic
findFirst-then-insert/update sequence that can race under concurrent writes.
Replace this branch and the equivalent logic in updateBoardSettings with a
shared dialect-aware Drizzle upsert helper using onConflictDoUpdate for
PostgreSQL/SQLite and the MySQL equivalent, preserving the existing setting-key
conflict behavior and merged next value.

In `@packages/boards/src/edit-mode.tsx`:
- Around line 15-28: Update requestBoardEditAction so both execution paths
handle rejected promises from BoardEditAction through the application’s existing
error-reporting mechanism instead of discarding them with void action().
Preserve the current SSR and cancellable-event behavior, and reuse the
established error-reporting symbol rather than introducing new handling.

In `@packages/modals/src/index.tsx`:
- Around line 127-131: The OpenModalOptions.onClose callback is declared but not
invoked when a modal closes. In packages/modals/src/index.tsx:127-131, update
the ActiveModal close flow, such as handleCloseModal or closeModal, to retrieve
and invoke the stored onClose callback for every dismissal path. In
packages/modals/src/confirm-modal.tsx:19, keep onClose options-only with no
direct component change, unless explicitly forwarding innerProps.onClose is
required by the chosen provider implementation.

In `@packages/request-handler/src/lib/request-handler.ts`:
- Line 8: Remove the duplicate RequestResult<TData> type alias declaration,
retaining only one definition used by the request-handler module so TypeScript
no longer reports a duplicate identifier.
- Around line 27-29: Update invalidateCache and the request/inflight resolution
logic to prevent pre-invalidation promises from writing cache entries or
deleting newer inflight requests. Track the cache generation or exact promise
identity, and only apply completion updates when they still belong to the
current generation/request. Add a regression test covering invalidate → refetch
→ old request resolves.

In `@packages/widgets/src/common/query-state.tsx`:
- Around line 40-73: Update WidgetDataState so the warning is accessible on
touch devices: either render the translated warning label inline, or configure
the Tooltip to respond to touch and focus while making the Center trigger
keyboard-focusable. Preserve the existing warning visibility condition and
styling, and ensure users on mobile can discover the stale/failed-data
indicator.

In `@packages/widgets/src/media-missing/component.tsx`:
- Around line 40-46: Update the media-missing widget’s mobile and desktop
rendering to detect partial integration failures with hasWidgetDataWarning using
expectedIntegrationCount and receivedIntegrationCount, rather than relying only
on error. Wrap the desktop tabbed view in WidgetDataState, and apply the same
warning state to the mobile summary while preserving the existing loading and
all-failures error behavior.

In `@packages/widgets/src/rssFeed/component.tsx`:
- Around line 57-65: Sanitize feedEntry.description before it reaches the
dangerouslySetInnerHTML sink in the RSS widget render. Update the description
rendering in the component to use an established sanitizer or safely re-encode
script-capable markup, while preserving the existing hideDescription, Text
props, and text content behavior.

---

Nitpick comments:
In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx:
- Around line 67-79: Remove the dead mobile gating from the desktop-only render
path in the header actions component: update the isEditMode AddMenu condition to
omit !isMobile and remove the always-false hidden={isMobile} prop from
EditModeMenu. Keep the existing early mobile return and its MobileMoreMenu
behavior unchanged.

In `@apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx`:
- Around line 27-57: The useEffect in DeferredMobileItem recreates its viewport
observer because onNearViewportChange changes identity on each render. Store the
latest callback in a ref-backed stable handler, invoke that handler from the
observer callback, and remove onNearViewportChange from the effect dependency
array so the observer lifecycle depends only on eager and unmountWhenOffscreen.

In `@apps/nextjs/src/components/board/mobile/mobile-presentation.ts`:
- Around line 18-19: Update the mobile presentation options so
unmountWhenOffscreen is disabled whenever the resolved eager value is true,
including when eager uses the app/bookmarks default. Base the condition on the
resolved eager expression or value rather than only mobile?.eager === true,
while preserving the existing fallback behavior for non-eager items.

In `@apps/nextjs/src/components/board/mobile/mobile-viewport-observer.spec.ts`:
- Around line 39-47: Reset the module-scoped observer state between tests in the
mobile viewport observer spec by using vi.resetModules() and dynamically
importing mobile-viewport-observer for each test, or by invoking an exported
test reset hook. Ensure observer, subscriptions, and resize-listener state is
freshly initialized before every test while preserving the existing
IntersectionObserverMock setup.

In `@apps/nextjs/src/components/board/use-mobile-board.spec.ts`:
- Around line 77-85: Move the shouldShowMobileBoardViewportSkeleton test out of
the resolveIsAutomaticMobileBoard describe block into a dedicated describe. Add
coverage for enableAutomaticMobileLayout true with isResolved false and assert
that the skeleton is shown, while preserving the existing legacy-mode assertion.

In `@apps/nextjs/src/components/board/use-mobile-board.tsx`:
- Around line 76-89: Rename hasResolvedClientWidth and its setter to hasMounted
and setHasMounted throughout the component, including the useEffect and
resolveMobileBoardViewport call. Preserve the existing mount-only initialization
and pass the renamed flag to the viewport resolver.

In `@e2e/mobile-board.spec.ts`:
- Around line 164-182: Add explicit scenario flags to each mobileViewports
descriptor for reduced-motion, zoom-overflow, and interaction/RTL checks, then
update the affected scenario branches to use viewport.reducedMotion,
viewport.checksZoomOverflow, and viewport.checksInteractions instead of
hard-coded width comparisons. Preserve the current scenario assignments for each
viewport while removing the fragile pixel-width coupling.

In `@packages/api/src/router/test/serverSettings.spec.ts`:
- Around line 133-153: The test “getBoardSettings should backfill the automatic
layout default for legacy values” currently cannot distinguish the backfill
behavior from the normal default. Seed the database with a saved responsive
layout before inserting the legacy board settings, then assert that
getBoardSettings returns the flipped enableAutomaticMobileLayout value while
preserving the other default board settings.

In `@packages/api/src/settle-integrations.ts`:
- Around line 22-28: Update getIntegrationQueryProvenance and its callers so
failedIntegrationCount is based on the original failed-query information rather
than results.length when settleIntegrationQueries uses fallback. Preserve
staleIntegrationCount from the returned results, and add a guard in the relevant
API or documentation to prevent combining fallback with provenance calculation
unless failure counts remain available.

In `@packages/boards/src/context.spec.ts`:
- Around line 16-36: Extend the “board layout selection” tests to cover
getCurrentLayout with legacy mode and an undefined viewport, asserting it falls
back to the desktop “large” layout. Also add a test for getCurrentLayout when
the board has no layouts, asserting it throws the “Board must have a layout”
error.

In `@packages/db/queries/server-setting.ts`:
- Around line 46-75: Update getServerSettingsAsync to reuse the already parsed
board settings from the defaultServerSettingsKeys reduce when checking
enableAutomaticMobileLayout, instead of parsing persistedBoardSettings.value
again. Preserve the existing fallback to
getAutomaticMobileLayoutUpgradeDefaultAsync when the reused value is not
boolean.
- Around line 16-32: The test suite should cover the duplicate canonical-layout
branch in shouldEnableAutomaticMobileLayoutForUpgrade by adding a case with two
Base layouts at breakpoint 0 sharing the same boardId, and assert that it
returns false.

In `@packages/widgets/src/media-requests/stats/component.tsx`:
- Around line 107-119: The mobile summary description in the displayMode ===
"mobileSummary" branch should use a translated ICU message instead of
concatenating pending and the localized label. Add a pending-count translation
key with a {pending} placeholder to the existing English translations, then pass
the pending value through that key in the WidgetMobileSummary description so
locales can control word order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c82b604-38c2-4034-a8be-047d17880948

📥 Commits

Reviewing files that changed from the base of the PR and between 472ea16 and 7959c20.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (200)
  • apps/docs/docs/management/api/index.mdx
  • apps/docs/docs/management/boards/index.mdx
  • apps/docs/docs/management/settings/index.mdx
  • apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.spec.ts
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.ts
  • apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx
  • apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx
  • apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
  • apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
  • apps/nextjs/src/app/[locale]/boards/layout.tsx
  • apps/nextjs/src/app/[locale]/layout.tsx
  • apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx
  • apps/nextjs/src/app/[locale]/manage/settings/_components/common-form.tsx
  • apps/nextjs/src/components/board/items/actions/create-item.ts
  • apps/nextjs/src/components/board/items/actions/empty-position.ts
  • apps/nextjs/src/components/board/items/actions/move-and-resize-item.ts
  • apps/nextjs/src/components/board/items/actions/move-item-to-section.ts
  • apps/nextjs/src/components/board/items/actions/test/create-item.spec.ts
  • apps/nextjs/src/components/board/items/actions/test/empty-position.spec.ts
  • apps/nextjs/src/components/board/items/actions/test/move-and-resize-item.spec.ts
  • apps/nextjs/src/components/board/items/actions/test/move-item-to-section.spec.ts
  • apps/nextjs/src/components/board/items/item-content.tsx
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.spec.ts
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-presentation.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.ts
  • apps/nextjs/src/components/board/mobile/mobile-viewport-observer.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-viewport-observer.ts
  • apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx
  • apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx
  • apps/nextjs/src/components/board/sections/dynamic-section.tsx
  • apps/nextjs/src/components/board/sections/gridstack/gridstack.tsx
  • apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts
  • apps/nextjs/src/components/board/sections/section-actions.tsx
  • apps/nextjs/src/components/board/use-mobile-board.spec.ts
  • apps/nextjs/src/components/board/use-mobile-board.tsx
  • apps/nextjs/src/components/language/current-language-combobox.tsx
  • apps/nextjs/src/components/layout/header.tsx
  • apps/nextjs/src/components/layout/header/button.tsx
  • apps/nextjs/src/components/layout/header/responsive-user.tsx
  • apps/nextjs/src/components/layout/header/search.tsx
  • apps/nextjs/src/components/layout/header/user.tsx
  • apps/nextjs/src/components/layout/shell.tsx
  • apps/nextjs/src/components/user-avatar-menu.tsx
  • e2e/mobile-board.spec.ts
  • packages/api/src/router/board.ts
  • packages/api/src/router/serverSettings.ts
  • packages/api/src/router/test/board.spec.ts
  • packages/api/src/router/test/serverSettings.spec.ts
  • packages/api/src/router/widgets/beszel.ts
  • packages/api/src/router/widgets/calendar.ts
  • packages/api/src/router/widgets/downloads.ts
  • packages/api/src/router/widgets/health-monitoring.ts
  • packages/api/src/router/widgets/media-release.ts
  • packages/api/src/router/widgets/media-requests.ts
  • packages/api/src/router/widgets/media-server.ts
  • packages/api/src/router/widgets/notifications.ts
  • packages/api/src/router/widgets/uptime-kuma.ts
  • packages/api/src/settle-integrations.spec.ts
  • packages/api/src/settle-integrations.ts
  • packages/api/src/test/mcp.spec.ts
  • packages/boards/package.json
  • packages/boards/src/context.spec.ts
  • packages/boards/src/context.tsx
  • packages/boards/src/edit-mode.spec.ts
  • packages/boards/src/edit-mode.tsx
  • packages/db/migrations/seed.spec.ts
  • packages/db/migrations/seed.ts
  • packages/db/queries/server-setting.ts
  • packages/modals/src/confirm-modal.tsx
  • packages/modals/src/index.tsx
  • packages/request-handler/src/lib/request-handler.spec.ts
  • packages/request-handler/src/lib/request-handler.ts
  • packages/request-handler/src/media-release.ts
  • packages/server-settings/src/index.ts
  • packages/settings/src/creator.ts
  • packages/spotlight/package.json
  • packages/spotlight/src/preferences/use-user-preference.ts
  • packages/translation/src/lang/en.json
  • packages/ui/src/index.ts
  • packages/ui/src/theme.ts
  • packages/ui/src/theme/layers.ts
  • packages/ui/src/theme/modal.spec.ts
  • packages/ui/src/theme/modal.ts
  • packages/validation/src/board.ts
  • packages/widgets/src/anchor-note/index.ts
  • packages/widgets/src/app/component.tsx
  • packages/widgets/src/app/index.ts
  • packages/widgets/src/archive-team-warrior/index.ts
  • packages/widgets/src/audio-stats/index.ts
  • packages/widgets/src/bazarr/index.ts
  • packages/widgets/src/beszel-alerts/component.tsx
  • packages/widgets/src/beszel-alerts/index.ts
  • packages/widgets/src/beszel-system-grid/component.tsx
  • packages/widgets/src/beszel-system-grid/index.ts
  • packages/widgets/src/beszel-system-stats/component.tsx
  • packages/widgets/src/beszel-system-stats/index.ts
  • packages/widgets/src/beszel-system-table/component.tsx
  • packages/widgets/src/beszel-system-table/index.ts
  • packages/widgets/src/beszel/_shared/error-indicator.tsx
  • packages/widgets/src/beszel/_shared/mobile-summary.tsx
  • packages/widgets/src/beszel/_shared/query-status.spec.ts
  • packages/widgets/src/beszel/_shared/query-status.ts
  • packages/widgets/src/beszel/_shared/stats-view.tsx
  • packages/widgets/src/bookmarks/component.tsx
  • packages/widgets/src/bookmarks/index.tsx
  • packages/widgets/src/calendar/component.tsx
  • packages/widgets/src/calendar/index.ts
  • packages/widgets/src/clock/index.ts
  • packages/widgets/src/common/mobile-summary.tsx
  • packages/widgets/src/common/query-state.spec.ts
  • packages/widgets/src/common/query-state.tsx
  • packages/widgets/src/coolify/applications-section.tsx
  • packages/widgets/src/coolify/component.tsx
  • packages/widgets/src/coolify/index.ts
  • packages/widgets/src/coolify/instance-card.tsx
  • packages/widgets/src/coolify/resource-row.tsx
  • packages/widgets/src/coolify/servers-section.tsx
  • packages/widgets/src/coolify/services-section.tsx
  • packages/widgets/src/coolify/single-instance-layout.tsx
  • packages/widgets/src/custom-api/component.tsx
  • packages/widgets/src/custom-api/index.ts
  • packages/widgets/src/custom-api/jsx-interactive-components.tsx
  • packages/widgets/src/definition.ts
  • packages/widgets/src/dns-hole/controls/TimerModal.tsx
  • packages/widgets/src/dns-hole/controls/component.tsx
  • packages/widgets/src/dns-hole/controls/index.ts
  • packages/widgets/src/dns-hole/summary/index.ts
  • packages/widgets/src/docker/component.tsx
  • packages/widgets/src/docker/index.ts
  • packages/widgets/src/downloads/component.tsx
  • packages/widgets/src/downloads/index.ts
  • packages/widgets/src/filter-storage-volumes.spec.ts
  • packages/widgets/src/filter-storage-volumes.ts
  • packages/widgets/src/firewall/index.ts
  • packages/widgets/src/health-monitoring/component.tsx
  • packages/widgets/src/health-monitoring/index.ts
  • packages/widgets/src/health-monitoring/system-health.tsx
  • packages/widgets/src/iframe/component.tsx
  • packages/widgets/src/iframe/index.ts
  • packages/widgets/src/index.tsx
  • packages/widgets/src/indexer-manager/index.ts
  • packages/widgets/src/media-missing/component.tsx
  • packages/widgets/src/media-missing/index.ts
  • packages/widgets/src/media-releases/component.tsx
  • packages/widgets/src/media-releases/index.ts
  • packages/widgets/src/media-requests/list/component.tsx
  • packages/widgets/src/media-requests/list/index.ts
  • packages/widgets/src/media-requests/stats/component.tsx
  • packages/widgets/src/media-requests/stats/index.ts
  • packages/widgets/src/media-server/component.tsx
  • packages/widgets/src/media-server/index.ts
  • packages/widgets/src/media-transcoding/component.tsx
  • packages/widgets/src/media-transcoding/index.ts
  • packages/widgets/src/notebook/component.tsx
  • packages/widgets/src/notebook/index.ts
  • packages/widgets/src/notebook/mobile-summary.spec.ts
  • packages/widgets/src/notebook/mobile-summary.ts
  • packages/widgets/src/notebook/notebook.tsx
  • packages/widgets/src/notifications/component.tsx
  • packages/widgets/src/notifications/index.ts
  • packages/widgets/src/paperless-ngx/index.ts
  • packages/widgets/src/patchmon/index.ts
  • packages/widgets/src/releases/index.ts
  • packages/widgets/src/rssFeed/component.tsx
  • packages/widgets/src/rssFeed/index.ts
  • packages/widgets/src/smart-home/entity-state/index.ts
  • packages/widgets/src/smart-home/execute-automation/index.ts
  • packages/widgets/src/speedtest-tracker/index.ts
  • packages/widgets/src/stocks/index.ts
  • packages/widgets/src/system-disks/component.tsx
  • packages/widgets/src/system-disks/index.ts
  • packages/widgets/src/system-disks/mobile-summary.spec.ts
  • packages/widgets/src/system-disks/mobile-summary.ts
  • packages/widgets/src/system-resources/component.tsx
  • packages/widgets/src/system-resources/index.ts
  • packages/widgets/src/timetable/index.ts
  • packages/widgets/src/tracearr/index.ts
  • packages/widgets/src/traefik/index.ts
  • packages/widgets/src/umami/index.ts
  • packages/widgets/src/ups/index.ts
  • packages/widgets/src/uptime-kuma/component.tsx
  • packages/widgets/src/uptime-kuma/index.ts
  • packages/widgets/src/vpn/index.ts
  • packages/widgets/src/weather/component.tsx
  • packages/widgets/src/weather/index.ts
  • packages/widgets/src/weather/temperature.spec.ts
  • packages/widgets/src/weather/temperature.ts
🚧 Files skipped from review as they are similar to previous changes (114)
  • apps/nextjs/src/components/board/mobile/deferred-mobile-item.spec.ts
  • apps/nextjs/src/components/layout/header/responsive-user.tsx
  • packages/widgets/src/weather/temperature.spec.ts
  • packages/widgets/src/beszel/_shared/query-status.ts
  • packages/widgets/src/calendar/index.ts
  • packages/widgets/src/system-disks/mobile-summary.spec.ts
  • packages/widgets/src/custom-api/index.ts
  • packages/widgets/src/audio-stats/index.ts
  • packages/widgets/src/beszel-system-stats/index.ts
  • packages/widgets/src/beszel/_shared/query-status.spec.ts
  • packages/widgets/src/beszel-system-grid/index.ts
  • packages/api/src/router/widgets/uptime-kuma.ts
  • apps/nextjs/src/components/layout/header/search.tsx
  • packages/widgets/src/notifications/index.ts
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.spec.ts
  • packages/widgets/src/ups/index.ts
  • packages/widgets/src/media-server/index.ts
  • packages/widgets/src/dns-hole/controls/index.ts
  • packages/widgets/src/firewall/index.ts
  • packages/widgets/src/weather/index.ts
  • packages/widgets/src/releases/index.ts
  • packages/widgets/src/system-disks/index.ts
  • packages/widgets/src/system-disks/mobile-summary.ts
  • packages/widgets/src/coolify/applications-section.tsx
  • packages/widgets/src/downloads/index.ts
  • packages/widgets/src/bazarr/index.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.ts
  • apps/nextjs/src/components/layout/header/user.tsx
  • packages/widgets/src/notebook/index.ts
  • packages/widgets/src/media-transcoding/index.ts
  • apps/nextjs/src/components/board/mobile/mobile-section-navigation.spec.ts
  • apps/nextjs/src/app/[locale]/boards/layout.tsx
  • packages/widgets/src/notebook/mobile-summary.spec.ts
  • packages/widgets/src/docker/index.ts
  • packages/widgets/src/system-resources/index.ts
  • packages/widgets/src/bookmarks/index.tsx
  • packages/api/src/router/widgets/beszel.ts
  • packages/widgets/src/media-releases/index.ts
  • packages/widgets/src/tracearr/index.ts
  • packages/widgets/src/vpn/index.ts
  • packages/widgets/src/indexer-manager/index.ts
  • packages/widgets/src/coolify/instance-card.tsx
  • packages/widgets/src/coolify/services-section.tsx
  • packages/widgets/src/beszel-system-table/index.ts
  • packages/widgets/src/umami/index.ts
  • packages/widgets/src/notebook/mobile-summary.ts
  • packages/widgets/src/iframe/index.ts
  • packages/widgets/src/anchor-note/index.ts
  • packages/widgets/src/paperless-ngx/index.ts
  • packages/widgets/src/index.tsx
  • packages/api/src/settle-integrations.spec.ts
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.spec.ts
  • apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
  • packages/widgets/src/media-requests/stats/index.ts
  • packages/widgets/src/app/index.ts
  • packages/widgets/src/rssFeed/index.ts
  • packages/widgets/src/beszel/_shared/mobile-summary.tsx
  • packages/widgets/src/common/mobile-summary.tsx
  • apps/nextjs/src/components/board/mobile/mobile-dialog-stack.ts
  • apps/nextjs/src/components/layout/header/button.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_prefetch-policy.ts
  • packages/widgets/src/weather/temperature.ts
  • packages/widgets/src/uptime-kuma/index.ts
  • apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
  • packages/widgets/src/filter-storage-volumes.spec.ts
  • packages/widgets/src/media-missing/index.ts
  • packages/widgets/src/notebook/notebook.tsx
  • packages/widgets/src/iframe/component.tsx
  • packages/widgets/src/timetable/index.ts
  • packages/widgets/src/smart-home/entity-state/index.ts
  • packages/widgets/src/clock/index.ts
  • packages/widgets/src/traefik/index.ts
  • apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
  • packages/widgets/src/coolify/index.ts
  • packages/widgets/src/media-releases/component.tsx
  • packages/widgets/src/app/component.tsx
  • packages/widgets/src/coolify/single-instance-layout.tsx
  • packages/api/src/router/widgets/media-server.ts
  • packages/widgets/src/beszel-alerts/index.ts
  • packages/widgets/src/dns-hole/controls/TimerModal.tsx
  • packages/widgets/src/coolify/component.tsx
  • packages/widgets/src/beszel/_shared/stats-view.tsx
  • packages/widgets/src/patchmon/index.ts
  • packages/widgets/src/coolify/resource-row.tsx
  • packages/widgets/src/smart-home/execute-automation/index.ts
  • packages/widgets/src/health-monitoring/system-health.tsx
  • apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
  • packages/widgets/src/custom-api/jsx-interactive-components.tsx
  • packages/widgets/src/media-requests/list/index.ts
  • packages/widgets/src/beszel-system-grid/component.tsx
  • packages/api/src/router/widgets/downloads.ts
  • packages/widgets/src/stocks/index.ts
  • packages/widgets/src/beszel/_shared/error-indicator.tsx
  • packages/api/src/router/widgets/calendar.ts
  • packages/widgets/src/media-transcoding/component.tsx
  • apps/nextjs/src/components/layout/header.tsx
  • packages/widgets/src/dns-hole/controls/component.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • packages/widgets/src/filter-storage-volumes.ts
  • packages/widgets/src/beszel-system-table/component.tsx
  • packages/widgets/src/beszel-alerts/component.tsx
  • packages/widgets/src/custom-api/component.tsx
  • packages/widgets/src/notifications/component.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • packages/widgets/src/beszel-system-stats/component.tsx
  • packages/widgets/src/coolify/servers-section.tsx
  • packages/widgets/src/media-server/component.tsx
  • packages/widgets/src/health-monitoring/component.tsx
  • packages/widgets/src/media-requests/list/component.tsx
  • packages/widgets/src/notebook/component.tsx
  • packages/widgets/src/system-disks/component.tsx
  • packages/widgets/src/downloads/component.tsx
  • apps/nextjs/src/components/board/items/item-content.tsx

Comment thread apps/docs/docs/management/api/index.mdx Outdated
Comment thread apps/docs/docs/management/boards/index.mdx
Comment thread apps/docs/docs/management/boards/index.mdx
Comment thread apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx Outdated
Comment thread packages/api/src/router/serverSettings.ts Outdated
Comment thread packages/request-handler/src/lib/request-handler.ts Outdated
Comment thread packages/request-handler/src/lib/request-handler.ts
Comment thread packages/widgets/src/common/query-state.tsx Outdated
Comment thread packages/widgets/src/media-missing/component.tsx Outdated
Comment thread packages/widgets/src/rssFeed/component.tsx Outdated
@ajnart

ajnart commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/api/src/router/serverSettings.ts (1)

58-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Explain how MCP callers obtain board IDs.

Line 58 defines which board IDs are valid, but not where callers retrieve them. Name the relevant public-board listing tool/API and its id field in the description.

As per coding guidelines, MCP descriptions must explain “how to obtain required IDs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/router/serverSettings.ts` at line 58, Update the MCP
description near the global board defaults definition to explain that callers
can obtain valid board IDs from the public-board listing tool/API, using each
returned board’s id field. Preserve the existing validation and optional-field
details.

Source: Coding guidelines

apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx (2)

190-203: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Edit toggle disappears mid-edit if the viewport crosses the mobile breakpoint.

hidden comes from useIsMobileBoard(), whose media query re-evaluates live after mount. If a user starts editing on a desktop-sized viewport and then resizes below the breakpoint, hidden flips to true, hiding the save/discard button while isEditMode (and any unsaved changes) is still active — the user is stuck until they resize back. Consider keeping the button visible whenever isEditMode is true, regardless of hidden.

♻️ Suggested tweak
-  if (hidden) return null;
+  if (hidden && !isEditMode) return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx around
lines 190 - 203, Update the visibility guard in the header actions around the
`isEditMode` and `hidden` checks so the edit toggle remains rendered whenever
`isEditMode` is true, even if `hidden` changes after a mobile breakpoint resize.
Preserve the existing hidden behavior when not editing and keep the `toggle`
callback’s save, discard, and open behavior unchanged.

245-303: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Anchor listeners are attached once and miss dynamically-rendered links.

document.querySelectorAll(anchorSelector) runs once per effect invocation (keyed only on isDirty), and listeners are bound only to the anchors that exist in the DOM at that moment. None of the Menu components in this file (SelectBoardsMenu, AddMenu's target, etc.) pass keepMounted, so per Mantine's default behavior their Menu.Dropdown content — including the board-switcher Menu.Item links — isn't in the DOM until first opened, and can be unmounted again after closing. Clicking any such link therefore bypasses openConfirmModal entirely and navigates away via router.push, silently discarding unsaved edits.

Prefer restoring event delegation on document with event.target.closest(anchorSelector) so newly-rendered anchors are always covered without needing to re-query the DOM.

Static analysis also flags the effect's missing deps (openConfirmModal, t, router); closures can go stale if these change while isDirty remains true.

🛡️ Suggested delegation-based fix
     const handleClick = (event: Event) => {
-      const target = (event.target as HTMLElement).closest("a");
+      const target = (event.target as HTMLElement).closest<HTMLAnchorElement>(anchorSelector);

       if (!target) {
-        console.warn("No anchor element found for click event", event);
         return;
       }

       event.preventDefault();
       ...
     };
     ...
-    const anchors = document.querySelectorAll(anchorSelector);
-    anchors.forEach((link) => {
-      link.addEventListener("click", handleClick);
-    });
+    document.addEventListener("click", handleClick);
     window.addEventListener("popstate", handlePopState);
     window.addEventListener("beforeunload", handleBeforeUnload);

     return () => {
-      anchors.forEach((link) => {
-        link.removeEventListener("click", handleClick);
-      });
+      document.removeEventListener("click", handleClick);
       window.removeEventListener("popstate", handlePopState);
       window.removeEventListener("beforeunload", handleBeforeUnload);
     };
-  }, [isDirty]);
+  }, [isDirty, openConfirmModal, t, router]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx around
lines 245 - 303, Update usePreventLeaveWithDirty to delegate anchor clicks from
document instead of querying and binding the current anchors, using
event.target.closest(anchorSelector) so dynamically mounted menu links are
intercepted. Register and clean up the document listener consistently, and
include openConfirmModal, t, and router in the effect dependencies so the
handler does not retain stale closures.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
apps/nextjs/src/components/board/mobile/mobile-board.module.css (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded /2 mirrors mobileColumnCount but isn't tied to it.

grid-auto-rows divides the viewport width by a literal 2 to approximate one column's width, duplicating the mobileColumnCount = 2 constant used elsewhere. If that constant ever changes, this CSS silently desyncs.

♻️ Suggested tweak
-  grid-auto-rows: min(calc((100vw - 42px) / 2), 12rem);
+  grid-auto-rows: min(calc((100vw - 42px) / var(--mobile-column-count)), 12rem);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nextjs/src/components/board/mobile/mobile-board.module.css` at line 5,
Update the mobile board grid sizing so the row calculation derives its
column-width divisor from the existing mobileColumnCount configuration rather
than a hardcoded 2. Keep the current spacing and 12rem maximum behavior
unchanged, and ensure the CSS remains synchronized when the column count
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/nextjs/src/app/`[locale]/boards/_layout-creator.tsx:
- Around line 67-69: Update the initialIsMobile calculation in the layout
creator to treat both "mobile" and "tablet" user-agent device types as mobile,
matching useIsMobileBoard() and preventing tablet requests from initially
rendering the desktop board.

---

Outside diff comments:
In `@apps/nextjs/src/app/`[locale]/boards/(content)/_header-actions.tsx:
- Around line 190-203: Update the visibility guard in the header actions around
the `isEditMode` and `hidden` checks so the edit toggle remains rendered
whenever `isEditMode` is true, even if `hidden` changes after a mobile
breakpoint resize. Preserve the existing hidden behavior when not editing and
keep the `toggle` callback’s save, discard, and open behavior unchanged.
- Around line 245-303: Update usePreventLeaveWithDirty to delegate anchor clicks
from document instead of querying and binding the current anchors, using
event.target.closest(anchorSelector) so dynamically mounted menu links are
intercepted. Register and clean up the document listener consistently, and
include openConfirmModal, t, and router in the effect dependencies so the
handler does not retain stale closures.

In `@packages/api/src/router/serverSettings.ts`:
- Line 58: Update the MCP description near the global board defaults definition
to explain that callers can obtain valid board IDs from the public-board listing
tool/API, using each returned board’s id field. Preserve the existing validation
and optional-field details.

---

Nitpick comments:
In `@apps/nextjs/src/components/board/mobile/mobile-board.module.css`:
- Line 5: Update the mobile board grid sizing so the row calculation derives its
column-width divisor from the existing mobileColumnCount configuration rather
than a hardcoded 2. Keep the current spacing and 12rem maximum behavior
unchanged, and ensure the CSS remains synchronized when the column count
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79bfb641-5b74-4334-87fb-e3f11ea64df8

📥 Commits

Reviewing files that changed from the base of the PR and between 7959c20 and b5ffefe.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .github/workflows/ci.yml
  • apps/docs/docs/management/api/index.mdx
  • apps/docs/docs/management/boards/index.mdx
  • apps/docs/docs/management/settings/index.mdx
  • apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
  • apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
  • apps/nextjs/src/components/board/mobile/mobile-board.module.css
  • apps/nextjs/src/components/board/mobile/mobile-board.tsx
  • apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
  • apps/nextjs/src/components/board/mobile/mobile-layout.ts
  • apps/nextjs/src/components/board/use-mobile-board.tsx
  • e2e/mobile-board.spec.ts
  • packages/api/src/router/serverSettings.ts
  • packages/api/src/router/test/serverSettings.spec.ts
  • packages/boards/src/context.spec.ts
  • packages/boards/src/edit-mode.tsx
  • packages/db/collection.ts
  • packages/db/migrations/seed.spec.ts
  • packages/db/queries/server-setting.ts
  • packages/db/test/schema.spec.ts
  • packages/translation/src/lang/en.json
💤 Files with no reviewable changes (1)
  • packages/boards/src/edit-mode.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/boards/src/context.spec.ts
  • packages/db/queries/server-setting.ts
  • packages/db/migrations/seed.spec.ts

Comment thread apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx
@ajnart

ajnart commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Feel free to try it @Nerdy-Technician

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-demo This PR needs a demo deployment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants