Skip to content

feat(frontend): add release links and mobile board layout#69

Merged
JustAGhosT merged 2 commits into
mainfrom
agent/release-task-links
Jul 12, 2026
Merged

feat(frontend): add release links and mobile board layout#69
JustAGhosT merged 2 commits into
mainfrom
agent/release-task-links

Conversation

@JustAGhosT

@JustAGhosT JustAGhosT commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add shareable release task URLs at /projects/:project/releases/:release
  • sync the release filter with the URL and add a copy-release-link button
  • make the board usable on phones with a project drawer, compact header, and one status column at a time
  • remove the mobile warning banner now that narrow layouts are supported

Validation

  • pnpm --dir frontend run build
  • pnpm --dir frontend run test
  • Playwright CLI smoke check at 390x844 for the local mobile shell

Summary by CodeRabbit

  • New Features
    • Added deep links that open a project directly to a specific release.
    • Added a button to copy the current release link.
    • Added mobile sidebar navigation with an accessible hamburger menu.
    • Added mobile board column switching for easier viewing on small screens.
  • Improvements
    • Updated responsive layouts and controls for better usability across screen sizes.
    • Removed the mobile warning banner in favor of the dedicated mobile interface.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The frontend adds release-based project deep links and release-link copying, introduces mobile sidebar navigation, hides desktop-only header controls on small screens, and provides single-column mobile Kanban navigation. The previous mobile warning gate now renders nothing.

Changes

Frontend routing and responsive layout

Layer / File(s) Summary
Release deep-linking and link sharing
frontend/src/App.tsx, frontend/src/components/Header.tsx
Project URLs parse and preserve release targets, switch grouping to release, and provide an encoded release-link copy action.
Mobile navigation and responsive header
frontend/src/App.tsx, frontend/src/components/Header.tsx
Mobile sidebar state and overlay rendering are added, while header controls receive responsive visibility and layout updates.
Mobile Kanban column navigation
frontend/src/components/KanbanBoard.tsx, frontend/src/components/MobileGate.tsx
Small screens receive a column selector and single-column display; column resizing is desktop-only, and MobileGate returns nothing.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant App
  participant Header
  participant KanbanBoard
  Browser->>App: Open project release URL
  App->>App: Parse release target and set release grouping
  App->>Header: Render selected release
  Header->>Browser: Copy encoded release link
  App->>KanbanBoard: Render board
  KanbanBoard->>KanbanBoard: Select visible mobile column
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: release links and the mobile board layout.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 agent/release-task-links

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d4304b0bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/src/App.tsx
Comment on lines +137 to +138
setReleaseTarget(urlReleaseTarget);
setBoardGrouping('release');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset saved view when applying release links

Opening a copied /projects/:project/releases/:release URL only overwrites releaseTarget/grouping here; the persisted baton.board.viewPreset remains active. If the recipient previously selected Blocked, Open high, etc., the shared release link shows only that subset (and on mobile the preset selector is hidden), so copied release URLs are not reliably shareable unless the preset is reset or encoded in the URL.

Useful? React with 👍 / 👎.

Comment on lines 318 to +320
<button
onClick={() => setAccountOpen(o => !o)}
className="text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[10rem] truncate"
className="hidden md:block text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[10rem] truncate"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose the account menu on mobile

When auth is enabled (or claims are present), this hidden button is the only control that opens the account menu and reaches the logout() action below; the new hidden md:block class removes it under 768px and the mobile drawer does not provide an alternative. Authenticated phone users therefore have no in-app way to sign out or verify which account is active.

Useful? React with 👍 / 👎.

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
frontend/src/App.tsx (1)

129-149: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale urlReleaseTarget/releaseTarget can survive URL resolution.

setUrlReleaseTarget(null) only fires inside the if (found) branch (Line 136-140). If the URL project key doesn't resolve to a project, urlProjectKey is nulled but urlReleaseTarget is left set — harmless but permanently dead state. More impactful: when a user opens a plain /projects/:key link (no release segment), urlReleaseTarget is null so the persisted releaseTarget from a prior session/project is never cleared, and the board can silently apply a stale release filter that isn't reflected in the URL the user actually navigated to.

🐛 Suggested fix
       if (found) {
         setSelectedProjectId(found.id);
-        if (urlReleaseTarget !== null) {
-          setReleaseTarget(urlReleaseTarget);
-          setBoardGrouping('release');
-          setUrlReleaseTarget(null);
-        }
+        if (urlReleaseTarget !== null) {
+          setReleaseTarget(urlReleaseTarget);
+          setBoardGrouping('release');
+        } else {
+          setReleaseTarget('');
+        }
+        setUrlReleaseTarget(null);
         return;
       }
🤖 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 `@frontend/src/App.tsx` around lines 129 - 149, Update the project URL
resolution effect to clear both URL-derived release state and the active release
filter whenever a project URL is resolved, including when no release target is
present; also clear urlReleaseTarget when the URL project key does not resolve.
Preserve applying setReleaseTarget and release grouping only when a valid
urlReleaseTarget is provided.
frontend/src/components/Header.tsx (1)

316-323: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sign-out becomes completely inaccessible on mobile.

The account trigger button (which exposes the "Sign out" action) is hidden md:block, so on any viewport below the md breakpoint there is no way to sign out at all — no equivalent control exists elsewhere (the hamburger only opens the project drawer). This is significant if config.requireAuth is enabled and a user needs to end their session on a shared/borrowed mobile device.

🐛 Suggested fix
           <button
             onClick={() => setAccountOpen(o => !o)}
-            className="hidden md:block text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[10rem] truncate"
+            className="text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[6rem] md:max-w-[10rem] truncate"
           >
🤖 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 `@frontend/src/components/Header.tsx` around lines 316 - 323, Update the
account trigger button in the account menu flow around accountRef and
accountLabel so it remains accessible on mobile; remove or replace the md-only
visibility restriction while preserving its existing click behavior and styling
intent, ensuring users can still open the menu and reach “Sign out” below the md
breakpoint.
🧹 Nitpick comments (5)
frontend/src/components/KanbanBoard.tsx (2)

496-508: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing selected-state semantics on mobile status buttons.

Selection is conveyed only via color; add aria-pressed so the current column is exposed to assistive tech, consistent with the Root toggle pattern already used in Header.tsx.

♻️ Suggested addition
           <button
             key={status}
             type="button"
             onClick={() => setMobileStatus(status)}
+            aria-pressed={mobileStatus === status}
             className={...}
🤖 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 `@frontend/src/components/KanbanBoard.tsx` around lines 496 - 508, The mobile
status buttons in KanbanBoard’s status selector lack selected-state semantics.
Update the button rendered for each status to include aria-pressed based on
whether mobileStatus equals status, matching the existing Root toggle pattern in
Header.tsx while preserving the current styling and click behavior.

511-606: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Off-screen mobile columns still fully render.

Non-selected columns are only CSS-hidden (hidden vs flex, Line 524); their bucket/priority grouping and task cards are still computed and mounted. On resource-constrained mobile devices with large boards, this means up to 3x the render work for content the user can't see.

Consider skipping body rendering for non-selected columns on mobile, e.g. gate the column's inner content on !isMobile || status === mobileStatus.

🤖 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 `@frontend/src/components/KanbanBoard.tsx` around lines 511 - 606, Update the
column rendering inside the COLUMNS.map loop to skip non-selected mobile column
bodies: only compute/render bucketed task content, add forms, task cards, and
empty-state content when !isMobile or status === mobileStatus. Keep the column
header and resize behavior intact, and preserve all existing content for the
selected mobile column and desktop columns.
frontend/src/App.tsx (3)

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

Consolidate duplicate URL-parsing regexes.

urlProjectKey and urlReleaseTarget each re-match the pathname with near-identical, easy-to-desync regexes (one optionally captures the release, the other requires it). Parsing the pathname once and deriving both values from a single match would remove the duplication and the risk of the two patterns drifting apart.

♻️ Suggested consolidation
+  const decodeSegment = (s: string) => {
+    try { return decodeURIComponent(s); } catch { return s; }
+  };
+  const initialUrlMatch = React.useRef(
+    window.location.pathname.match(/^\/projects\/([^/]+)(?:\/releases\/([^/]+))?$/),
+  ).current;
   const [urlProjectKey, setUrlProjectKey] = useState<string | null>(() =>
-    ...
+    initialUrlMatch ? decodeSegment(initialUrlMatch[1]) : null
   );
   const [urlReleaseTarget, setUrlReleaseTarget] = useState<string | null>(() =>
-    ...
+    initialUrlMatch?.[2] ? decodeSegment(initialUrlMatch[2]) : 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 `@frontend/src/App.tsx` around lines 59 - 81, Consolidate the pathname parsing
for urlProjectKey and urlReleaseTarget into a single match during their initial
state setup, capturing the project and optional release segments together.
Decode each captured segment independently with the existing malformed-encoding
fallbacks, and derive both state values from that shared match instead of
re-running separate regular expressions.

287-306: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mobile drawer lacks dialog semantics / Escape-to-close.

The overlay works by tap-outside-to-close, but has no role="dialog"/aria-modal, no focus trap, and no Escape key handler, so keyboard/screen-reader users have no way to dismiss it other than selecting a project.

🤖 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 `@frontend/src/App.tsx` around lines 287 - 306, Update the mobile sidebar
drawer rendered under mobileSidebarOpen to expose dialog semantics with
role="dialog" and aria-modal, close when Escape is pressed, and trap focus
within the drawer while open. Keep the existing backdrop click and
project-selection close behavior unchanged, anchoring the implementation to the
mobile overlay and its inner Sidebar wrapper.

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

Duplicated Sidebar JSX between desktop and mobile.

The desktop (269-286) and mobile (288-306) blocks render the same Sidebar with identical props aside from onSelect, onOpenSettings, width, and onResize. Extracting the shared props into one object would remove ~15 lines of duplication and keep both variants in sync going forward.

♻️ Suggested refactor
+        {(() => {
+          const sharedSidebarProps = {
+            projects: visibleProjects,
+            archivedCount: projects.length - projects.filter(p => p.status === 'active').length,
+            showArchived,
+            onToggleArchived: setShowArchived,
+            selectedId: selectedProjectId,
+            selectedLabelIds,
+            onLabelIdsChange: setSelectedLabelIds,
+            onCreate: async (data: Parameters<typeof createProject>[0]) => { await createProject(data); },
+            onDelete: deleteProject,
+          };
+          return (
+            <>
               {sidebarOpen && (
                 <div className="hidden md:flex flex-shrink-0">
-                  <Sidebar ... />
+                  <Sidebar {...sharedSidebarProps} onSelect={setSelectedProjectId} onOpenSettings={setSettingsProjectId} width={sidebarWidth} onResize={setSidebarWidth} />
                 </div>
               )}
               {mobileSidebarOpen && (
                 <div className="md:hidden fixed inset-0 z-50 bg-black/60" onClick={() => setMobileSidebarOpen(false)}>
                   <div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}>
-                    <Sidebar ... />
+                    <Sidebar {...sharedSidebarProps} onSelect={(id) => { setSelectedProjectId(id); setMobileSidebarOpen(false); }} onOpenSettings={(id) => { setSettingsProjectId(id); setMobileSidebarOpen(false); }} width={360} onResize={() => {}} />
                   </div>
                 </div>
               )}
+            </>
+          );
+        })()}
🤖 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 `@frontend/src/App.tsx` around lines 268 - 307, Refactor the duplicated Sidebar
usage in the desktop and mobile branches of the App component by extracting
their identical props into a shared props object or equivalent reusable
configuration. Keep the existing variant-specific onSelect, onOpenSettings,
width, and onResize behavior for each branch while ensuring both Sidebar
instances remain synchronized.
🤖 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 `@frontend/src/App.tsx`:
- Around line 288-306: Replace the hardcoded width passed to Sidebar in the
mobile drawer with a value that fits the responsive wrapper, preserving the 88vw
width and 360px maximum constraint. Update the mobile Sidebar invocation and
keep the desktop sizing behavior unchanged.

In `@frontend/src/components/KanbanBoard.tsx`:
- Around line 494-510: Update the mobile column-switcher count in the COLUMNS
map to use visibleColumnTasks(status).length instead of byStatus(status).length,
keeping the displayed badge consistent with the filtered column content when
rootOnly is enabled.

---

Outside diff comments:
In `@frontend/src/App.tsx`:
- Around line 129-149: Update the project URL resolution effect to clear both
URL-derived release state and the active release filter whenever a project URL
is resolved, including when no release target is present; also clear
urlReleaseTarget when the URL project key does not resolve. Preserve applying
setReleaseTarget and release grouping only when a valid urlReleaseTarget is
provided.

In `@frontend/src/components/Header.tsx`:
- Around line 316-323: Update the account trigger button in the account menu
flow around accountRef and accountLabel so it remains accessible on mobile;
remove or replace the md-only visibility restriction while preserving its
existing click behavior and styling intent, ensuring users can still open the
menu and reach “Sign out” below the md breakpoint.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 59-81: Consolidate the pathname parsing for urlProjectKey and
urlReleaseTarget into a single match during their initial state setup, capturing
the project and optional release segments together. Decode each captured segment
independently with the existing malformed-encoding fallbacks, and derive both
state values from that shared match instead of re-running separate regular
expressions.
- Around line 287-306: Update the mobile sidebar drawer rendered under
mobileSidebarOpen to expose dialog semantics with role="dialog" and aria-modal,
close when Escape is pressed, and trap focus within the drawer while open. Keep
the existing backdrop click and project-selection close behavior unchanged,
anchoring the implementation to the mobile overlay and its inner Sidebar
wrapper.
- Around line 268-307: Refactor the duplicated Sidebar usage in the desktop and
mobile branches of the App component by extracting their identical props into a
shared props object or equivalent reusable configuration. Keep the existing
variant-specific onSelect, onOpenSettings, width, and onResize behavior for each
branch while ensuring both Sidebar instances remain synchronized.

In `@frontend/src/components/KanbanBoard.tsx`:
- Around line 496-508: The mobile status buttons in KanbanBoard’s status
selector lack selected-state semantics. Update the button rendered for each
status to include aria-pressed based on whether mobileStatus equals status,
matching the existing Root toggle pattern in Header.tsx while preserving the
current styling and click behavior.
- Around line 511-606: Update the column rendering inside the COLUMNS.map loop
to skip non-selected mobile column bodies: only compute/render bucketed task
content, add forms, task cards, and empty-state content when !isMobile or status
=== mobileStatus. Keep the column header and resize behavior intact, and
preserve all existing content for the selected mobile column and desktop
columns.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a71cccdf-1928-4e1b-a55d-46bc577cf093

📥 Commits

Reviewing files that changed from the base of the PR and between c6f780c and 6d4304b.

📒 Files selected for processing (4)
  • frontend/src/App.tsx
  • frontend/src/components/Header.tsx
  • frontend/src/components/KanbanBoard.tsx
  • frontend/src/components/MobileGate.tsx

Comment thread frontend/src/App.tsx
Comment on lines +288 to +306
<div className="md:hidden fixed inset-0 z-50 bg-black/60" onClick={() => setMobileSidebarOpen(false)}>
<div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}>
<Sidebar
projects={visibleProjects}
archivedCount={projects.length - projects.filter(p => p.status === 'active').length}
showArchived={showArchived}
onToggleArchived={setShowArchived}
selectedId={selectedProjectId}
onSelect={(id) => { setSelectedProjectId(id); setMobileSidebarOpen(false); }}
selectedLabelIds={selectedLabelIds}
onLabelIdsChange={setSelectedLabelIds}
onCreate={async (data) => { await createProject(data); }}
onDelete={deleteProject}
onOpenSettings={(id) => { setSettingsProjectId(id); setMobileSidebarOpen(false); }}
width={360}
onResize={() => {}}
/>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fixed width={360} can exceed the 88vw drawer container on narrow screens.

The drawer wrapper is w-[88vw] max-w-[360px], but Sidebar receives a hardcoded width={360} which it applies via inline style={{ width }}. At the PR's own smoke-test viewport (390px), 88vw = 343.2px < 360px, so the aside renders wider than its wrapper and visually overflows the intended drawer bounds by ~17px.

🐛 Suggested fix
-            <div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}>
+            <div className="h-full w-[88vw] max-w-[360px] overflow-hidden" onClick={e => e.stopPropagation()}>
               <Sidebar
                 ...
-                width={360}
+                width={Math.min(360, Math.round(window.innerWidth * 0.88))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="md:hidden fixed inset-0 z-50 bg-black/60" onClick={() => setMobileSidebarOpen(false)}>
<div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}>
<Sidebar
projects={visibleProjects}
archivedCount={projects.length - projects.filter(p => p.status === 'active').length}
showArchived={showArchived}
onToggleArchived={setShowArchived}
selectedId={selectedProjectId}
onSelect={(id) => { setSelectedProjectId(id); setMobileSidebarOpen(false); }}
selectedLabelIds={selectedLabelIds}
onLabelIdsChange={setSelectedLabelIds}
onCreate={async (data) => { await createProject(data); }}
onDelete={deleteProject}
onOpenSettings={(id) => { setSettingsProjectId(id); setMobileSidebarOpen(false); }}
width={360}
onResize={() => {}}
/>
</div>
</div>
<div className="md:hidden fixed inset-0 z-50 bg-black/60" onClick={() => setMobileSidebarOpen(false)}>
<div className="h-full w-[88vw] max-w-[360px] overflow-hidden" onClick={e => e.stopPropagation()}>
<Sidebar
projects={visibleProjects}
archivedCount={projects.length - projects.filter(p => p.status === 'active').length}
showArchived={showArchived}
onToggleArchived={setShowArchived}
selectedId={selectedProjectId}
onSelect={(id) => { setSelectedProjectId(id); setMobileSidebarOpen(false); }}
selectedLabelIds={selectedLabelIds}
onLabelIdsChange={setSelectedLabelIds}
onCreate={async (data) => { await createProject(data); }}
onDelete={deleteProject}
onOpenSettings={(id) => { setSettingsProjectId(id); setMobileSidebarOpen(false); }}
width={Math.min(360, Math.round(window.innerWidth * 0.88))}
onResize={() => {}}
/>
</div>
</div>
🤖 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 `@frontend/src/App.tsx` around lines 288 - 306, Replace the hardcoded width
passed to Sidebar in the mobile drawer with a value that fits the responsive
wrapper, preserving the 88vw width and 360px maximum constraint. Update the
mobile Sidebar invocation and keep the desktop sizing behavior unchanged.

Comment on lines +494 to +510
<div className="md:hidden flex gap-1 border-b border-[#1a1a1a] px-2 py-2 overflow-x-auto">
{COLUMNS.map(({ status, label }) => (
<button
key={status}
type="button"
onClick={() => setMobileStatus(status)}
className={`flex-1 min-w-24 rounded-md border px-3 py-2 text-xs transition-colors ${
mobileStatus === status
? 'border-amber-500/30 bg-amber-500/10 text-amber-400'
: 'border-[#1f1f1f] bg-[#0a0a0a] text-zinc-500'
}`}
>
{label}
<span className="ml-1 text-[10px] text-zinc-600">{byStatus(status).length}</span>
</button>
))}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mobile column-switcher counts ignore rootOnly filtering.

The pill counts use byStatus(status).length (Line 507), the raw unfiltered task count, while the actual rendered column content uses visibleColumnTasks(status) (Line 516), which excludes children when rootOnly is true (the default). On mobile, the badge will over-count relative to what's actually shown in that column for any project with subtasks.

🐛 Suggested fix
-            <span className="ml-1 text-[10px] text-zinc-600">{byStatus(status).length}</span>
+            <span className="ml-1 text-[10px] text-zinc-600">{visibleColumnTasks(status).length}</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="md:hidden flex gap-1 border-b border-[#1a1a1a] px-2 py-2 overflow-x-auto">
{COLUMNS.map(({ status, label }) => (
<button
key={status}
type="button"
onClick={() => setMobileStatus(status)}
className={`flex-1 min-w-24 rounded-md border px-3 py-2 text-xs transition-colors ${
mobileStatus === status
? 'border-amber-500/30 bg-amber-500/10 text-amber-400'
: 'border-[#1f1f1f] bg-[#0a0a0a] text-zinc-500'
}`}
>
{label}
<span className="ml-1 text-[10px] text-zinc-600">{byStatus(status).length}</span>
</button>
))}
</div>
<div className="md:hidden flex gap-1 border-b border-[`#1a1a1a`] px-2 py-2 overflow-x-auto">
{COLUMNS.map(({ status, label }) => (
<button
key={status}
type="button"
onClick={() => setMobileStatus(status)}
className={`flex-1 min-w-24 rounded-md border px-3 py-2 text-xs transition-colors ${
mobileStatus === status
? 'border-amber-500/30 bg-amber-500/10 text-amber-400'
: 'border-[`#1f1f1f`] bg-[`#0a0a0a`] text-zinc-500'
}`}
>
{label}
<span className="ml-1 text-[10px] text-zinc-600">{visibleColumnTasks(status).length}</span>
</button>
))}
</div>
🤖 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 `@frontend/src/components/KanbanBoard.tsx` around lines 494 - 510, Update the
mobile column-switcher count in the COLUMNS map to use
visibleColumnTasks(status).length instead of byStatus(status).length, keeping
the displayed badge consistent with the filtered column content when rootOnly is
enabled.

@JustAGhosT JustAGhosT merged commit 29709e6 into main Jul 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant