Skip to content

[Console Redesign / OBT-258] Reimplement the console UI on the Shema design - #40

Open
levigtri wants to merge 94 commits into
mainfrom
levigft/obt-258-console-redesign-reimplement-the-ui-to-match-the-new-shema
Open

[Console Redesign / OBT-258] Reimplement the console UI on the Shema design#40
levigtri wants to merge 94 commits into
mainfrom
levigft/obt-258-console-redesign-reimplement-the-ui-to-match-the-new-shema

Conversation

@levigtri

@levigtri levigtri commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes OBT-258.

Summary

Reimplements the console UI to match the approved Shema design prototype (Tripod Console.dc.html), wired to the real tripod-api. The prototype runs on a mock data.js plus a custom sc-if/sc-for templating runtime; this branch translates it into the real React 18 + TypeScript + Vite + Tailwind v4 front.

No mock data — every screen binds to tripod-api through the existing src/services/api.ts namespaces and its JWT interceptors.

This branch supersedes and contains levigft/obt-259-... (public requests / admin review), so that branch does not need its own PR.

Screens

Public request (/request), Dashboard (My Apps + Platform Overview), Languages, Projects + Project detail (Info / Phases / Access, location map), Users + User detail, Manage Apps + App detail, Phases (catalog + dependency graph, admin-only), Map, and the shared chrome (sidebar rail, header, dialogs, toasts).

Notable behaviour changes

  • Organizations is removed from the console (OBT-234 / US-12.5). The section, its routes and its nav entry are gone.
  • Console access gate: the /app shell requires platform admin or manager; anyone else gets AccessDeniedPage. Admin-only routes (/app/users, /app/apps, /app/phases) stay wrapped in AdminRoute.
  • Apps use a platforms array (web / android / ios) with one badge per platform, replacing the single platform field.
  • Project cards render a project image tile, an "X/Y phases completed" progress bar and an overlapping member avatar stack (photo, or initials with a hashed palette fallback).

Backend dependency

The project cards need image_url, phases_completed, phases_total, members_preview and team_size on ProjectResponse. Those come from tripod-api PR #117 (OBT-262) — which already contains OBT-216's team_size — plus its migration 20260716_0002. Without that API, the Projects page renders but the progress bar and avatar stack have nothing to show.

Upload fix included

userDetail uploaded admin-set photos with folder=user-avatars, which is not in the API's allowlist, so they were silently stored under the generic images/ prefix instead of alongside the ones ProfileDialog uploads. Now sends avatars.

Map popup and language usage (design follow-up)

  • Map popup: the phase chips are tinted by phase status (not started / in progress / completed / blocked) instead of highlighting only the in-progress phase — previously every other status rendered as the same grey chip, so the card looked unchanged as a project advanced. The colors come from a shared PHASE_STATUS_CONFIG that the project phases graph also uses. The card now follows the prototype's popupHtml: plain location line, up to four chips (+N for the rest), no divider above the meta line, a pill "Open project" button, and the dark CARTO tiles in dark mode.
  • Languages: the Projects column rendered a hardcoded , so there was no way to see how many projects use a language. It now shows that count per row. The deactivate dialog is titled after the language and, when the language is in use, lists those projects in an accent-soft callout so the platform admin confirms with the usage in front of them. Deactivation stays allowed — it is gated by the admin role, not by usage, matching tripod-api #97.

The dialog needs tripod-api #97 (soft delete, require_platform_admin) and #98 (GET /languages/{id}/stats, admin-only) — neither is on main yet. The prototype blocks an in-use deactivation with a 409; the real rule is admin-only with no usage block, so the console follows the API and only surfaces a 409 as a toast if some deployment still refuses.

Test plan

  • tsc -b and npm run lint pass clean.
  • Sign in as a platform admin: sidebar shows Main / Content / Administration; every screen renders on the Shema tokens in both light and dark.
  • Sign in as a manager: Content is visible, Administration is not; the console still loads.
  • Sign in as a plain member: AccessDeniedPage with the logout variant.
  • Projects page against tripod-api #117: cards show the progress bar, the member avatar stack and the image tile.
  • Upload a photo from User detail and from the profile dialog; both land under avatars/.
  • /app/map: open a marker popup — the phase chips are tinted per status; change a phase status under Project detail → Phases, go back to the map and the chip color follows.
  • /app/languages as a platform admin: the Projects column shows a count per language; deactivating a language that has projects lists them in the dialog and still goes through; deactivating an unused language works too. A manager never sees the deactivate action.

Note on uploads while testing locally: image upload needs a valid GCS service-account key. If GOOGLE_APPLICATION_CREDENTIALS points at a missing file, the API answers 503 with STORAGE_UNAVAILABLE (see tripod-api OBT-263) rather than a bare 500.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a public request page for submitting project/language requests, with optional reCAPTCHA.
    • Rolled out role-aware navigation plus an admin “needs review” panel with live pending counts.
    • Introduced reusable filtering/search UI and refreshed map/project selection experience.
  • Bug Fixes
    • Improved authentication/session handling and protected-route loading/access-denied behavior.
  • Style
    • Refreshed the design system across the app (new tokens, component styling, and updated dark-mode/focus visuals).

levigtri added 30 commits July 4, 2026 19:23
Removes the Organizations sidebar item and its Building2 icon import, the /app/organizations
and /app/organizations/:orgId routes and their imports in App.tsx, and deletes the
OrganizationsPage and OrganizationDetailPage components. Unknown organization paths now fall
through to the NotFound (404) page.

orgsAPI is kept in services/api.ts — it is still used by the project Access tab and the admin
dashboard.
AppShell now denies the whole /app subtree to authenticated users who are neither platform
admins nor managers, rendering the Access Denied page instead of the console content.
Unauthenticated users are still redirected to /login and the loading state is unchanged.

AccessDeniedPage gains a "logout" variant offering a Sign out action (instead of the Go to
Dashboard link) so a fully denied user is not looped back into the gated shell.
…rm admins

On the project Access tab, the per-user Role control is now interactive only for platform
admins; other users (including managers) see the role as read-only text. Role changes still go
through projectsAPI.updateUserRole, with the backend as the source of truth (US-11.9). Grant and
revoke visibility is unchanged.
AuthContext now fetches the caller's managed project IDs (GET /api/auth/my-managed-projects) on
login and session restore, exposes managedProjectIds, and derives isManager from managed projects
or organizations. This matches the backend's per-project manager rule (US-11.6/11.8), so a user
who manages projects but no organization is recognized as a manager and is no longer wrongly
denied by the console shell gate (US-12.6) or hidden from the content navigation (US-12.2).
Replace the single `platform: string | null` field with `platforms: string[]`
across AppResponse, AppCreate, AppUpdate and UserAppResponse to match the new
multi-platform API schema (US-4.1).

Keep the existing single-select forms working via a legacy<->array bridge at the
API boundary; the multi-select UI (US-4.3) and per-platform badges (US-4.4) build
on top of this change.
Replace the single Web/Mobile/Both dropdown in the app create and edit forms
with a PlatformMultiSelect toggle group (Web, Android, iOS) that allows any
combination. Require at least one platform before saving, pre-select the current
platforms when editing, and send the selection as an array.

Remove the transitional legacy<->array bridge introduced in US-4.2.
Show an individual badge for each entry in the app's platforms array on the
dashboard app cards, the Manage Apps grid, and the app detail page, instead of a
single combined label. Centralize the platform display labels in
constants/platforms and drop the old platformLabel switch in AppCard.
Add the team_size field to ProjectResponse (US-9.1) and display it in the map
popup's details grid with a people icon and a "N members" label, so users can
gauge a project's scale at a glance.
Add a "New language" action next to the language selector that opens a nested
dialog with name and code fields. On save it creates the language via the API,
refreshes the languages store, and auto-selects the new entry — the project form
state is preserved throughout. Duplicate-code errors are surfaced to the user.
Introduce a shared FilterBar with configurable filter dropdowns (label, options,
value, change handler), an optional search input, and an optional result label.
Active filters are highlighted with the telha accent, and the standalone Filter
icon is dropped to reduce visual noise. Styled with the Shema design tokens.
Replace the inline search + app filter JSX with the shared FilterBar component,
passing the existing search and app-filter state as props. Behavior is unchanged.
Replace the inline app + status filter JSX with the shared FilterBar component,
passing filterApp and filterStatus as filter configs and the request count as
the result label. Behavior is unchanged.
Remove the permanent right-hand dependency column. The phases page now uses a
single full-width layout (graph + table), and selecting a phase — from the table
row or a graph node — opens a dialog to manage its dependencies. The
DependencyPanel drops its standalone card wrapper since it now lives in the modal.
Display the selected phase's description (read-only) above its dependency list,
with a subtle "No description" placeholder when empty, so users get full context
without opening the edit dialog.
Add an SVG <title> to each phase node so hovering over it reveals the phase
description as a native browser tooltip, falling back to "No description" when
empty. This does not interfere with clicking or dragging the graph.
Add an edit affordance (pencil on hover) to each language card that opens the
shared dialog pre-filled with the language's name and code. Saving calls the new
languagesAPI.update (PUT), refreshes the store, and surfaces duplicate-code (409)
errors. Add the LanguageUpdate type and updated_at to LanguageResponse (US-1.1).
Add an admin-only deactivate affordance to each language card. Clicking it fetches
the language usage stats and shows the impact ("used in N projects") in a
confirmation dialog before soft-deleting via languagesAPI.delete. Add
languagesAPI.delete/stats, the LanguageStatsResponse type, and is_active on
LanguageResponse (US-2.1/2.2).
Add an "X/Y completed" summary next to the Phases heading in the map popup,
computed from the phases already loaded for the project, so users can gauge
overall progress at a glance.
Use a Globe icon for the web launch button (instead of a generic external-link),
an Apple icon for iOS, and keep Smartphone for Android so the two mobile buttons
are visually distinct and instantly recognizable.
Add a hover overlay over the image preview with clear icon actions — a camera to
change and an X to remove — so users can discover the actions instead of missing
the small text buttons. Works for both the square and circle shape variants and
applies everywhere ImageUpload is used.
…-frontend-manager-model-to-per-project' into local/integration-test
…ript-types-to-reflect-the-new-platforms' into local/integration-test
…tiple-platforms-independently-web-android' into local/integration-test
…dual-platform-badges-for-each-app-eg-web' into local/integration-test
…-buttons-to-show-platform-specific-icons' into local/integration-test
…remove-actions-to-be-visually-obvious' into local/integration-test
…-well-designed-filter-bar-component' into local/integration-test
…-bar-to-look-polished-and-consistent' into local/integration-test
…-bar-to-look-polished-and-consistent' into local/integration-test
…ase-description-alongside-its-dependencies' into local/integration-test
levigtri and others added 3 commits July 16, 2026 22:07
Match the prototype's project card and detail header:

- progress bar with "X/Y phases completed" from the enriched payload
- up to four overlapping member avatars, photo or hashed initials
- language code chip stays lowercase mono; "Updated Jul 8, 2026"
- detail header: upload and remove a project image, and resolve the language
  directly when the store misses it, so it no longer reads "Unknown language"
- split the info form and location card out of the page

Refs OBT-262

Co-Authored-By: Claude Fable 5 <[email protected]>
- users: "Platform admin" label, solid role badges, legend dots that match
  them, "N app roles", and a count on the access-requests tab
- project access: the role control reads as a pill — solid telha for manager,
  muted for member — and rows show real avatars
- apps: tiles fall back to hashed initials instead of one flat olive
- reviewing a request refreshes the pending counts, so the badges settle

Co-Authored-By: Claude Fable 5 <[email protected]>
The user detail page sent folder=user-avatars, which is not in the API's
allowlist, so admin-set photos were silently stored under the generic
images/ prefix instead of alongside the ones ProfileDialog uploads.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@linear-code

linear-code Bot commented Jul 17, 2026

Copy link
Copy Markdown

OBT-258

@coderabbitai

coderabbitai Bot commented Jul 17, 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

The pull request adds public language/project requests with optional reCAPTCHA, introduces change-request review flows, expands role-aware administration, refactors app/project/user/dashboard/map/phase pages into reusable components, and replaces legacy styling with semantic light/dark design tokens.

Changes

Console foundation and shared UI

Layer / File(s) Summary
Shared foundation and visual system
.env.example, src/index.css, src/components/ui/*, src/components/common/*, src/styles/*
Adds reCAPTCHA configuration, semantic theme tokens, dark-mode values, global focus styling, refreshed shared controls, reusable filters, avatars, image uploads, and common states.
Contracts, services, and stores
src/types/*, src/services/api.ts, src/contexts/AuthContext.tsx, src/stores/*
Adds request, role, platform, language, project, and phase contracts; expands API wrappers; centralizes session loading; and adds cache reset and pending-request count stores.

Public requests and reviews

Layer / File(s) Summary
Public request submission
src/components/pages/publicRequest/*, src/components/common/ReCaptcha.tsx, src/App.tsx
Adds public language/project request forms, language search, requester validation, optional reCAPTCHA, status-specific errors, confirmation output, and the /request route.
Request review workflow
src/components/pages/changeRequests/*, src/components/pages/ChangeRequestsSection.tsx
Normalizes authenticated and public requests, displays status-filtered cards, submits approvals/rejections, supports manager-access grants for project requests, and refreshes related stores.

Administration and management pages

Layer / File(s) Summary
RBAC navigation and access management
src/components/layout/*, src/components/pages/AccessDeniedPage.tsx, src/components/pages/AccessRequestsSection.tsx, src/components/pages/ProjectAccessTab.tsx, src/components/pages/projectAccess/*, src/components/pages/userDetail/*, src/components/pages/UsersPage/*
Adds role-aware shell access, dynamic sidebar sections and badges, project user/organization access controls, user role management, avatar/account actions, and access-request refresh behavior.
Application and project management
src/components/pages/AppsPage.tsx, src/components/pages/AppDetailPage.tsx, src/components/pages/apps/*, src/components/pages/ProjectsPage.tsx, src/components/pages/ProjectDetailPage.tsx, src/components/pages/projects/*
Changes app platforms to arrays, extracts app/project cards and dialogs, adds app deletion, project image/location editing, project request flows, language selection, and project filtering.
Dashboard and map composition
src/components/pages/DashboardPage.tsx, src/components/pages/dashboard/*, src/components/pages/MapPage.tsx, src/components/pages/map/*
Replaces dashboard sections with reusable app, map, statistics, and review panels and extracts searchable map rows and project popup content.
Phase management
src/components/pages/PhasesPage.tsx, src/components/pages/ProjectPhasesTab.tsx, src/components/pages/phases/*
Types phase statuses, removes attach/detach interactions, adds required-by dependency inspection, and updates graph, table, status, and dependency controls.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a console UI redesign aligned to the Shema design.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch levigft/obt-258-console-redesign-reimplement-the-ui-to-match-the-new-shema

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: 22

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

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

⚠️ Outside diff range comments (1)
src/components/pages/phases/PhaseFlowGraph.tsx (1)

334-375: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make interactive phase nodes keyboard-accessible.

Both graph implementations expose essential actions through mouse-only elements:

  • src/components/pages/phases/PhaseFlowGraph.tsx#L334-L375: add focusability, button semantics, an accessible name, and Enter/Space handling to each SVG node.
  • src/components/pages/ProjectPhasesTab.tsx#L184-L200: render the clickable phase card as a native button or provide equivalent keyboard behavior.
🤖 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 `@src/components/pages/phases/PhaseFlowGraph.tsx` around lines 334 - 375, Make
the phase node group rendered in PhaseFlowGraph.tsx keyboard-accessible by
adding focusability, button semantics, an accessible name, and Enter/Space key
handling that invokes handleNodeClick like the existing click handler. Also
update the clickable phase card in ProjectPhasesTab.tsx to use a native button
or equivalent keyboard interaction, preserving its current activation behavior.
🟡 Other comments (13)
src/components/pages/dashboard/NeedsReviewPanel.tsx-78-82 (1)

78-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not route “All requests” to the access-only destination.

This panel aggregates access, change, and public requests, but this link always opens /app/users; change/public reviews live under projects or languages. Remove the link, relabel it “Access requests,” or route it to an actual combined review page.

🤖 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 `@src/components/pages/dashboard/NeedsReviewPanel.tsx` around lines 78 - 82,
Update the “All requests” Link in the NeedsReviewPanel heading so it does not
route to the access-only /app/users destination; either remove the link, relabel
it “Access requests” to match that route, or point it to an existing combined
review page covering access, change, and public requests.
src/components/pages/dashboard/AdminStatsRow.tsx-19-34 (1)

19-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Label public requests explicitly.

changeCount includes pendingPublic, but the subtitle reports the entire value as “change,” producing incorrect category counts.

Proposed fix
- subtitle={`${accessCount} access · ${changeCount} change`}
+ subtitle={`${accessCount} access · ${changeCount} change/public`}
🤖 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 `@src/components/pages/dashboard/AdminStatsRow.tsx` around lines 19 - 34,
Update the pending reviews subtitle in AdminStatsRow so pendingPublic requests
are reported as a distinct category instead of being included under the change
count. Keep changeCount’s existing total contribution and add the corresponding
public-request count to the displayed breakdown.
src/components/pages/publicRequest/ProjectRequestForm.tsx-90-100 (1)

90-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the server’s 409 conflict detail.

The fixed language-conflict message can misreport duplicate project requests, especially when no new language is proposed. Use detail with a neutral fallback.

Proposed fix
 if (response?.status === 409) {
-  toast.error(detail || "This language already exists or was already requested")
+  toast.error(detail || "A matching project or language request already exists")
🤖 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 `@src/components/pages/publicRequest/ProjectRequestForm.tsx` around lines 90 -
100, Update the 409 handling in ProjectRequestForm to display the
server-provided detail when available, with a neutral fallback message that
covers any conflict rather than assuming a language duplicate. Leave the
existing 429, 422, and 400 handling unchanged.
src/components/common/FilterBar.tsx-42-47 (1)

42-47: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add aria-label to the search input for screen reader accessibility.

The search input relies solely on placeholder for identification, which is not a reliable accessible name. The adjacent Search icon is decorative and not associated with the input. Add an aria-label to ensure screen readers announce the input's purpose.

♿ Proposed fix
           <input
+            aria-label={search.placeholder ?? "Search"}
             placeholder={search.placeholder ?? "Search..."}
             value={search.value}
             onChange={(e) => search.onChange(e.target.value)}
             className="bg-transparent border-0 outline-none text-[13.5px] text-fg-strong placeholder:text-fg-subtle w-full"
           />
🤖 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 `@src/components/common/FilterBar.tsx` around lines 42 - 47, Update the search
input in FilterBar’s input rendering to include an explicit aria-label
describing its search purpose, while preserving the existing placeholder, value,
and onChange behavior.
src/components/common/LocationSearchInput.tsx-128-134 (1)

128-134: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give the clear-location button an accessible name.

The icon-only button is announced without a purpose by assistive technology. Add aria-label="Clear location".

🤖 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 `@src/components/common/LocationSearchInput.tsx` around lines 128 - 134, Add
aria-label="Clear location" to the icon-only button rendering the X icon in
LocationSearchInput, while preserving its existing handleClear behavior and
styling.
src/components/common/UserAvatar.tsx-21-24 (1)

21-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the image-error state when avatarUrl changes.

After one image fails, subsequent valid avatar URLs remain hidden until the component remounts.

Proposed fix
-import { useState } from "react"
+import { useEffect, useState } from "react"

 export function UserAvatar({ id, name, email, avatarUrl, size = "md", className }: UserAvatarProps) {
   const [imgError, setImgError] = useState(false)
+
+  useEffect(() => {
+    setImgError(false)
+  }, [avatarUrl])
🤖 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 `@src/components/common/UserAvatar.tsx` around lines 21 - 24, Update UserAvatar
so its imgError state resets whenever avatarUrl changes, allowing a newly
provided URL to render after a previous image failure. Add the reset alongside
the existing state logic while preserving the current showImage condition for
unchanged URLs.
src/components/pages/PhasesPage.tsx-213-220 (1)

213-220: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make selectable phase rows keyboard-accessible.

The click-only row cannot be focused or activated by keyboard users.

Proposed fix
 <tr
   key={phase.id}
+  tabIndex={0}
+  aria-selected={selectedPhaseId === phase.id}
   className={cn(
-    "cursor-pointer transition-colors hover:bg-muted",
+    "cursor-pointer transition-colors hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-[-2px]",
     selectedPhaseId === phase.id && "bg-muted",
   )}
   onClick={() => setSelectedPhaseId(phase.id)}
+  onKeyDown={(event) => {
+    if (event.key === "Enter" || event.key === " ") {
+      event.preventDefault()
+      setSelectedPhaseId(phase.id)
+    }
+  }}
 >
🤖 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 `@src/components/pages/PhasesPage.tsx` around lines 213 - 220, Make the phase
rows rendered in the PhasesPage table keyboard-accessible by adding focusability
and keyboard activation alongside the existing onClick handler. Update the row
identified by key={phase.id} to respond to Enter and Space by selecting the
phase, while preserving mouse selection and the existing visual styling.
src/components/pages/userDetail/AccountCard.tsx-29-38 (1)

29-38: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give the account-status switch an accessible name.

The adjacent status text is not associated with the Switch, leaving the control unlabeled for screen readers.

Proposed fix
-        <Switch checked={user.is_active} onCheckedChange={onToggleActive} />
+        <Switch
+          checked={user.is_active}
+          onCheckedChange={onToggleActive}
+          aria-label="Account active"
+        />
🤖 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 `@src/components/pages/userDetail/AccountCard.tsx` around lines 29 - 38, The
Switch in AccountCard needs an accessible name because the adjacent status text
is not associated with it. Update the Switch usage in the account-status section
to provide an accessible label, reusing the existing Active/Inactive status
context without changing the onToggleActive behavior.
src/components/pages/ProjectsPage.tsx-92-100 (1)

92-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the role-aware label in the empty state.

For managers, this action opens the request workflow, but it is labeled “Create Project.”

Proposed fix
-        actionLabel="Create Project"
+        actionLabel={isPlatformAdmin ? "Create Project" : "Request 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 `@src/components/pages/ProjectsPage.tsx` around lines 92 - 100, Update the
EmptyState action label in the projectsView empty-state branch to use the
role-aware label that matches openCreateDialog’s workflow, showing the
request-oriented text for managers while preserving the create-project label for
other roles.
src/components/layout/Sidebar.tsx-182-186 (1)

182-186: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fetch review counts for managers too.

Lines 198–205 render language and project badges for managers, but this effect only populates them for platform admins. Manager sessions therefore show empty or stale pending-review counts.

-    if (isPlatformAdmin) {
+    if (isPlatformAdmin || isManager) {
       void fetchCounts()
     }
-  }, [isPlatformAdmin, fetchCounts, pathname])
+  }, [isPlatformAdmin, isManager, fetchCounts, pathname])
🤖 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 `@src/components/layout/Sidebar.tsx` around lines 182 - 186, Update the
useEffect that invokes fetchCounts so it runs for both platform admins and
managers, using the existing manager-access condition alongside isPlatformAdmin.
Preserve the current dependency tracking and avoid restricting count fetching to
platform-admin sessions.
src/components/ui/button.tsx-11-17 (1)

11-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore destructive-action styling.

The shared destructive variant is identical to the primary variant, while app deletion explicitly uses the primary variant:

  • src/components/ui/button.tsx#L11-L17: give destructive a distinct danger treatment.
  • src/components/pages/apps/DangerZoneCard.tsx#L10-L17: use variant="destructive" for “Delete app…”.
🤖 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 `@src/components/ui/button.tsx` around lines 11 - 17, The shared destructive
variant in src/components/ui/button.tsx lines 11-17 must use distinct danger
styling instead of duplicating the default variant. Update the delete-app button
usage in src/components/pages/apps/DangerZoneCard.tsx lines 10-17 to set
variant="destructive", while preserving the existing button behavior.
src/components/pages/userDetail/GlobalRoleCard.tsx-25-56 (1)

25-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the selected role programmatically.

These custom radio-like buttons only render a visual indicator. Use a radiogroup with aria-checked, or add aria-pressed={selected}, so assistive technology can identify the current role.

🤖 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 `@src/components/pages/userDetail/GlobalRoleCard.tsx` around lines 25 - 56,
Expose selection state in the roleChoices button controls by adding
aria-pressed={selected} to each button, or alternatively implement a radiogroup
with aria-checked. Preserve the existing selected styling and roleSaving
behavior while ensuring assistive technology can identify the current role.
src/components/pages/projectAccess/GrantUserDialog.tsx-61-76 (1)

61-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Provide programmatic names for the newly introduced controls. The controls currently rely on visual labels, initials, or title, which do not consistently expose their intended action.

  • src/components/pages/projectAccess/GrantUserDialog.tsx#L61-L76: associate the role Label and SelectTrigger using htmlFor and id.
  • src/components/pages/userDetail/UserHeader.tsx#L20-L42: add an explicit upload-photo aria-label.
  • src/components/pages/apps/AutoApproveCard.tsx#L20-L24: associate a label or add an auto-approve aria-label.
  • src/components/pages/ProjectDetailPage.tsx#L191-L200: add an explicit upload-project-image aria-label.
🤖 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 `@src/components/pages/projectAccess/GrantUserDialog.tsx` around lines 61 - 76,
Provide programmatic names for all newly introduced controls: in
src/components/pages/projectAccess/GrantUserDialog.tsx lines 61-76, connect the
role Label and SelectTrigger with matching htmlFor and id values; in
src/components/pages/userDetail/UserHeader.tsx lines 20-42, add an explicit
upload-photo aria-label; in src/components/pages/apps/AutoApproveCard.tsx lines
20-24, associate the control with its label or add an auto-approve aria-label;
and in src/components/pages/ProjectDetailPage.tsx lines 191-200, add an explicit
upload-project-image aria-label.
🤖 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 `@src/App.tsx`:
- Line 55: Update the phases route in App.tsx to use an authorization wrapper
that permits users when isPlatformAdmin or isManager is true, preserving
platform-admin access while allowing managers to edit the global phase catalog;
only change the page documentation/copy instead if the intended contract is
admin-only.

In `@src/components/layout/Sidebar.tsx`:
- Around line 240-263: Update the mobile drawer rendered by the mobileOpen
branch in SidebarContent usage to provide modal dialog behavior: trap focus
within the open drawer, close it on Escape, restore focus to the triggering
element after closing, and expose appropriate dialog semantics. Prefer the
existing dialog/sheet primitive if available; otherwise implement equivalent
focus management while preserving the current onMobileClose behavior.

In `@src/components/pages/apps/AppCard.tsx`:
- Around line 26-31: Replace the non-focusable onClick container in AppCard with
a semantic link or button for opening the app, preserving its existing styling
and action. In src/components/pages/apps/AppCard.tsx lines 77-92, make
edit/delete controls visible on keyboard focus and devices without hover. In
src/components/pages/projects/ProjectCard.tsx lines 33-44, make project
navigation keyboard-operable and reveal the editing action on focus.

In `@src/components/pages/apps/DetailsCard.tsx`:
- Around line 65-72: Update the save-action validation in the form containing
the Platforms `PlatformMultiSelect` so it is disabled whenever `form.platforms`
is empty, enforcing the displayed minimum of one platform. Preserve the existing
save behavior when at least one platform is selected, including the
corresponding validation path noted near the alternate save control.

In `@src/components/pages/apps/RolesCard.tsx`:
- Around line 70-90: Update the role label and role_key inputs in RolesCard to
provide a visible keyboard focus indicator instead of only removing the outline.
Prefer replacing them with the shared Input component; otherwise add the
established focus-visible ring styling to both inputs while preserving their
existing behavior and appearance.

In `@src/components/pages/ChangeRequestsSection.tsx`:
- Around line 55-79: Update fetchRequests in ChangeRequestsSection to guard
against out-of-order responses by sequencing or cancelling each fetch, and only
apply results from the currently active request. Clear the existing requests
when the active fetch fails, including public-request loading failures as
appropriate, while preserving loading-state cleanup. Add useRef to the React
imports if using a sequence guard.

In `@src/components/pages/dashboard/MapPreview.tsx`:
- Around line 41-53: The MapPreview MapContainer currently disables Leaflet
attribution without providing a replacement. Restore visible basemap attribution
by enabling the attribution control or configuring equivalent attribution on the
TileLayer, using the existing tileUrl flow in MapContainer and TileLayer.

In `@src/components/pages/dashboard/useAdminDashboardData.ts`:
- Around line 39-47: Update the pending-request loading in useAdminDashboardData
so accessRequestsAPI, changeRequestsAPI, and publicRequestsAPI failures remain
distinguishable from successful empty responses. Preserve per-source error or
unknown state, and prevent the dashboard from reporting definitive zero pending
reviews until each source has loaded successfully.

In `@src/components/pages/DashboardPage.tsx`:
- Line 29: Update the data-loading flow in DashboardPage so rejected
Promise.allSettled results are inspected explicitly instead of treating them as
empty data through setApps([]). Expose the existing error/retry state for any
rejected API request, or replace allSettled with Promise.all if both requests
must succeed before rendering.

In `@src/components/pages/LanguagesPage.tsx`:
- Around line 146-177: Update the delete-dialog state around openDeleteDialog
and handleDeactivate to track statistics loading/error status and the request’s
target language identity. Ignore stale stats responses from previously selected
languages, and only enable the deactivation action when stats loaded
successfully with deleteStats.language_id matching deleteTarget.id; keep it
disabled while loading, after failure, or without a valid target.
- Around line 201-260: Update the languages table wrapper around the table in
LanguagesPage so horizontal overflow uses overflow-x-auto instead of
overflow-hidden, preserving the existing styling while allowing narrow viewports
to scroll to the status and action columns.

In `@src/components/pages/map/FieldMapPanel.tsx`:
- Around line 35-36: The map panel rendered by FieldMapPanel currently has no
mobile collapse behavior and obscures map content on narrow screens. Add a
responsive mobile layout with a visible close/collapse control, restoring the
previous collapsed behavior or implementing a bottom-sheet presentation, while
preserving the existing desktop panel layout.

In `@src/components/pages/projectAccess/OrgAccessSection.tsx`:
- Around line 12-70: Remove the organization access workflow: delete the
OrgAccessSection component and its grant/revoke callers, remove GrantOrgDialog
and all references to it, and delete the organization-inheritance copy from the
UserAccessSection empty state. Apply these changes in
src/components/pages/projectAccess/OrgAccessSection.tsx (12-70),
src/components/pages/projectAccess/GrantOrgDialog.tsx (21-90), and
src/components/pages/projectAccess/UserAccessSection.tsx (71-76).

In `@src/components/pages/projects/LocationSection.tsx`:
- Line 80: Update the manual-coordinate save flow around onSave so it also
synchronizes the local location state used by the marker and displayed
coordinates with the newly saved latitude, longitude, and name. Apply the same
update to the related coordinate-application paths, while preserving the
existing persistence behavior.
- Around line 67-80: Update handleSave to validate coordinates as a complete
pair: reject cases where only one of manualLat or manualLng is provided, require
the entire trimmed values to be valid numeric input rather than accepting
parseFloat prefixes, and enforce latitude between -90 and 90 and longitude
between -180 and 180 before calling onSave.

In `@src/components/pages/projects/ProjectFormDialog.tsx`:
- Around line 93-109: Update the platform-admin submission flow in
ProjectFormDialog so creating a new language and its project uses a single
transactional API operation rather than separate languagesAPI.create and
projectsAPI.create calls. Preserve the existing trimmed language/project fields
and language state refresh behavior, but route the combined creation through the
available transactional project-creation API and ensure either both records are
created or neither is persisted.

In `@src/components/pages/userDetail/index.tsx`:
- Around line 195-203: Update handleRoleSelect so selecting the platform_admin
role opens an explicit confirmation dialog instead of immediately calling
applyRoleUpdate. Add or reuse confirmation state and ensure the update is
submitted only after the user confirms, while preserving the existing manager
and member confirmation flows.
- Around line 71-99: Update the userId effect and the fetchUser/fetchRoles flows
to reset user and roles state and their loading flags immediately when the
identity changes, then abort or ignore any response associated with an obsolete
userId. Ensure stale requests cannot call setUser, setRoles, or leave loading
state for the newly routed user, while preserving the existing fetch error
handling and current-user behavior.

In `@src/components/pages/UsersPage/UserCard.tsx`:
- Around line 18-22: Update the clickable card in UserCard to use a
keyboard-accessible navigation element, preferably a Link targeting
`/app/users/${user.id}`, instead of the onClick-only div. Preserve the existing
styling and card content while removing the non-semantic click navigation.

In `@src/components/ui/dialog.tsx`:
- Line 41: Restore visible keyboard focus indicators on both
DialogPrimitive.Close in src/components/ui/dialog.tsx:41-41 and SelectTrigger in
src/components/ui/select.tsx:17-17 by adding equivalent focus-visible ring
styling, such as a 2px accent ring, alongside the existing focus outline
removal.

In `@src/index.css`:
- Around line 49-60: Update the CSS declarations in the affected sections to
satisfy Stylelint: lowercase the flagged font-family keywords, insert the
required empty line before margin declarations, and rename the keyframes to
kebab-case while updating every corresponding animation reference. Preserve the
existing styling and behavior.

In `@src/stores/phasesStore.ts`:
- Around line 51-52: Update reset() in phasesStore to invalidate any in-flight
fetches by aborting their requests or advancing a generation token. In fetch(),
verify the request generation is still current before applying phases,
dependencies, loading state, or lastFetched via set, discarding responses from
older generations so logout cannot repopulate the cache.

---

Outside diff comments:
In `@src/components/pages/phases/PhaseFlowGraph.tsx`:
- Around line 334-375: Make the phase node group rendered in PhaseFlowGraph.tsx
keyboard-accessible by adding focusability, button semantics, an accessible
name, and Enter/Space key handling that invokes handleNodeClick like the
existing click handler. Also update the clickable phase card in
ProjectPhasesTab.tsx to use a native button or equivalent keyboard interaction,
preserving its current activation behavior.

---

Other comments:
In `@src/components/common/FilterBar.tsx`:
- Around line 42-47: Update the search input in FilterBar’s input rendering to
include an explicit aria-label describing its search purpose, while preserving
the existing placeholder, value, and onChange behavior.

In `@src/components/common/LocationSearchInput.tsx`:
- Around line 128-134: Add aria-label="Clear location" to the icon-only button
rendering the X icon in LocationSearchInput, while preserving its existing
handleClear behavior and styling.

In `@src/components/common/UserAvatar.tsx`:
- Around line 21-24: Update UserAvatar so its imgError state resets whenever
avatarUrl changes, allowing a newly provided URL to render after a previous
image failure. Add the reset alongside the existing state logic while preserving
the current showImage condition for unchanged URLs.

In `@src/components/layout/Sidebar.tsx`:
- Around line 182-186: Update the useEffect that invokes fetchCounts so it runs
for both platform admins and managers, using the existing manager-access
condition alongside isPlatformAdmin. Preserve the current dependency tracking
and avoid restricting count fetching to platform-admin sessions.

In `@src/components/pages/dashboard/AdminStatsRow.tsx`:
- Around line 19-34: Update the pending reviews subtitle in AdminStatsRow so
pendingPublic requests are reported as a distinct category instead of being
included under the change count. Keep changeCount’s existing total contribution
and add the corresponding public-request count to the displayed breakdown.

In `@src/components/pages/dashboard/NeedsReviewPanel.tsx`:
- Around line 78-82: Update the “All requests” Link in the NeedsReviewPanel
heading so it does not route to the access-only /app/users destination; either
remove the link, relabel it “Access requests” to match that route, or point it
to an existing combined review page covering access, change, and public
requests.

In `@src/components/pages/PhasesPage.tsx`:
- Around line 213-220: Make the phase rows rendered in the PhasesPage table
keyboard-accessible by adding focusability and keyboard activation alongside the
existing onClick handler. Update the row identified by key={phase.id} to respond
to Enter and Space by selecting the phase, while preserving mouse selection and
the existing visual styling.

In `@src/components/pages/projectAccess/GrantUserDialog.tsx`:
- Around line 61-76: Provide programmatic names for all newly introduced
controls: in src/components/pages/projectAccess/GrantUserDialog.tsx lines 61-76,
connect the role Label and SelectTrigger with matching htmlFor and id values; in
src/components/pages/userDetail/UserHeader.tsx lines 20-42, add an explicit
upload-photo aria-label; in src/components/pages/apps/AutoApproveCard.tsx lines
20-24, associate the control with its label or add an auto-approve aria-label;
and in src/components/pages/ProjectDetailPage.tsx lines 191-200, add an explicit
upload-project-image aria-label.

In `@src/components/pages/ProjectsPage.tsx`:
- Around line 92-100: Update the EmptyState action label in the projectsView
empty-state branch to use the role-aware label that matches openCreateDialog’s
workflow, showing the request-oriented text for managers while preserving the
create-project label for other roles.

In `@src/components/pages/publicRequest/ProjectRequestForm.tsx`:
- Around line 90-100: Update the 409 handling in ProjectRequestForm to display
the server-provided detail when available, with a neutral fallback message that
covers any conflict rather than assuming a language duplicate. Leave the
existing 429, 422, and 400 handling unchanged.

In `@src/components/pages/userDetail/AccountCard.tsx`:
- Around line 29-38: The Switch in AccountCard needs an accessible name because
the adjacent status text is not associated with it. Update the Switch usage in
the account-status section to provide an accessible label, reusing the existing
Active/Inactive status context without changing the onToggleActive behavior.

In `@src/components/pages/userDetail/GlobalRoleCard.tsx`:
- Around line 25-56: Expose selection state in the roleChoices button controls
by adding aria-pressed={selected} to each button, or alternatively implement a
radiogroup with aria-checked. Preserve the existing selected styling and
roleSaving behavior while ensuring assistive technology can identify the current
role.

In `@src/components/ui/button.tsx`:
- Around line 11-17: The shared destructive variant in
src/components/ui/button.tsx lines 11-17 must use distinct danger styling
instead of duplicating the default variant. Update the delete-app button usage
in src/components/pages/apps/DangerZoneCard.tsx lines 10-17 to set
variant="destructive", while preserving the existing button 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: QUIET

Plan: Pro Plus

Run ID: 3ce9a9d8-9b3f-4139-baf6-032ec29abbd0

📥 Commits

Reviewing files that changed from the base of the PR and between 130fc4a and c978612.

⛔ Files ignored due to path filters (4)
  • public/assets/icon-branco.svg is excluded by !**/*.svg
  • public/assets/icon-telha.svg is excluded by !**/*.svg
  • public/assets/logo-branco.svg is excluded by !**/*.svg
  • public/assets/logo-verde.svg is excluded by !**/*.svg
📒 Files selected for processing (119)
  • .env.example
  • .gitignore
  • CLAUDE.md
  • eslint.config.js
  • index.html
  • src/App.tsx
  • src/components/common/EmptyState.tsx
  • src/components/common/ErrorBoundary.tsx
  • src/components/common/FeatureSpotlight.tsx
  • src/components/common/FilterBar.tsx
  • src/components/common/ImageUpload.tsx
  • src/components/common/InfoTooltip.tsx
  • src/components/common/LoadingSpinner.tsx
  • src/components/common/LocationSearchInput.tsx
  • src/components/common/PlatformMultiSelect.tsx
  • src/components/common/ProfileDialog.tsx
  • src/components/common/ReCaptcha.tsx
  • src/components/common/StatCard.tsx
  • src/components/common/UserAvatar.tsx
  • src/components/common/UserSearchPicker.tsx
  • src/components/layout/AppHeader.tsx
  • src/components/layout/AppShell.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/pages/AccessDeniedPage.tsx
  • src/components/pages/AccessRequestsSection.tsx
  • src/components/pages/AppDetailPage.tsx
  • src/components/pages/AppsPage.tsx
  • src/components/pages/ChangeRequestsSection.tsx
  • src/components/pages/DashboardPage.tsx
  • src/components/pages/LanguagesPage.tsx
  • src/components/pages/LoginPage.tsx
  • src/components/pages/MapPage.tsx
  • src/components/pages/NotFoundPage.tsx
  • src/components/pages/OrganizationDetailPage.tsx
  • src/components/pages/OrganizationsPage.tsx
  • src/components/pages/PhasesPage.tsx
  • src/components/pages/ProjectAccessTab.tsx
  • src/components/pages/ProjectDetailPage.tsx
  • src/components/pages/ProjectPhasesTab.tsx
  • src/components/pages/ProjectsPage.tsx
  • src/components/pages/ReviewDialog.tsx
  • src/components/pages/UserDetailPage.tsx
  • src/components/pages/UsersPage/UserCard.tsx
  • src/components/pages/UsersPage/index.tsx
  • src/components/pages/apps/AppCard.tsx
  • src/components/pages/apps/AppFormDialog.tsx
  • src/components/pages/apps/AutoApproveCard.tsx
  • src/components/pages/apps/DangerZoneCard.tsx
  • src/components/pages/apps/DetailsCard.tsx
  • src/components/pages/apps/RolesCard.tsx
  • src/components/pages/changeRequests/ChangeRequestCard.tsx
  • src/components/pages/changeRequests/MyChangeRequestsSection.tsx
  • src/components/pages/changeRequests/reviewableRequest.ts
  • src/components/pages/dashboard/AdminDashboard.tsx
  • src/components/pages/dashboard/AdminStatsRow.tsx
  • src/components/pages/dashboard/AppCard.tsx
  • src/components/pages/dashboard/MapPreview.tsx
  • src/components/pages/dashboard/MyAppsCard.tsx
  • src/components/pages/dashboard/NeedsReviewPanel.tsx
  • src/components/pages/dashboard/useAdminDashboardData.ts
  • src/components/pages/map/FieldMapPanel.tsx
  • src/components/pages/map/ProjectPopupContent.tsx
  • src/components/pages/phases/DependencyPanel.tsx
  • src/components/pages/phases/PhaseFlowGraph.tsx
  • src/components/pages/projectAccess/GrantOrgDialog.tsx
  • src/components/pages/projectAccess/GrantUserDialog.tsx
  • src/components/pages/projectAccess/OrgAccessSection.tsx
  • src/components/pages/projectAccess/RevokeButton.tsx
  • src/components/pages/projectAccess/UserAccessSection.tsx
  • src/components/pages/projectAccess/initials.ts
  • src/components/pages/projects/LocationSection.tsx
  • src/components/pages/projects/ProjectCard.tsx
  • src/components/pages/projects/ProjectFormDialog.tsx
  • src/components/pages/projects/ProjectInfoForm.tsx
  • src/components/pages/publicRequest/LanguageCombobox.tsx
  • src/components/pages/publicRequest/LanguageRequestForm.tsx
  • src/components/pages/publicRequest/ProjectRequestForm.tsx
  • src/components/pages/publicRequest/index.tsx
  • src/components/pages/userDetail/AccountCard.tsx
  • src/components/pages/userDetail/AppRolesCard.tsx
  • src/components/pages/userDetail/GlobalRoleCard.tsx
  • src/components/pages/userDetail/UserHeader.tsx
  • src/components/pages/userDetail/index.tsx
  • src/components/pages/userDetail/roles.ts
  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/card.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/input.tsx
  • src/components/ui/label.tsx
  • src/components/ui/popover.tsx
  • src/components/ui/select.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/tooltip.tsx
  • src/constants/platforms.ts
  • src/contexts/AuthContext.tsx
  • src/index.css
  • src/services/api.ts
  • src/stores/languagesStore.ts
  • src/stores/phasesStore.ts
  • src/stores/requestCountsStore.ts
  • src/styles/badges.ts
  • src/styles/cards.ts
  • src/styles/layout.ts
  • src/styles/states.ts
  • src/types/app.ts
  • src/types/auth.ts
  • src/types/changeRequest.ts
  • src/types/index.ts
  • src/types/language.ts
  • src/types/phase.ts
  • src/types/project.ts
  • src/types/publicRequest.ts
  • src/types/user.ts
  • src/utils/avatar.ts
  • src/utils/format.ts
  • src/vite-env.d.ts
💤 Files with no reviewable changes (4)
  • src/components/pages/dashboard/AdminDashboard.tsx
  • src/components/pages/UserDetailPage.tsx
  • src/components/pages/OrganizationDetailPage.tsx
  • src/components/pages/OrganizationsPage.tsx

Comment thread src/App.tsx
<Route path="apps" element={<AdminRoute><AppsPage /></AdminRoute>} />
<Route path="apps/:appId" element={<AdminRoute><AppDetailPage /></AdminRoute>} />
<Route path="phases" element={<PhasesPage />} />
<Route path="phases" element={<AdminRoute><PhasesPage /></AdminRoute>} />

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

Align phase routing with the manager-editable contract.

AdminRoute rejects every non-platform-admin, while PhasesPage says managers may edit the global phase catalog. Use an authorization wrapper accepting isPlatformAdmin || isManager, or update the documented requirement and page copy if phases are intentionally admin-only.

🤖 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 `@src/App.tsx` at line 55, Update the phases route in App.tsx to use an
authorization wrapper that permits users when isPlatformAdmin or isManager is
true, preserving platform-admin access while allowing managers to edit the
global phase catalog; only change the page documentation/copy instead if the
intended contract is admin-only.

Comment on lines +240 to 263
{mobileOpen && (
<div className="fixed inset-0 z-40 lg:hidden">
<div className="absolute inset-0 bg-preto/50 animate-fade-in" onClick={onMobileClose} />
<aside className="relative z-50 h-full w-[min(258px,calc(100vw-3rem))] bg-shell text-shell-fg flex flex-col px-3.5 pt-[22px] pb-4">
<button
onClick={onMobileClose}
aria-label="Close menu"
className="absolute right-3 top-3 w-8 h-8 rounded-[9px] grid place-items-center text-[var(--shell-dim)] hover:bg-[var(--shell-active)] hover:text-shell-fg transition-colors"
>
<X className="w-4 h-4" strokeWidth={2} />
</button>
<SidebarContent
sections={sections}
onNavigate={onMobileClose}
onProfile={openProfile}
onLogout={logout}
userId={user?.id}
userName={userName}
userEmail={user?.email ?? ""}
userRole={userRole}
avatarUrl={user?.avatar_url}
/>
</aside>
</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

Give the mobile drawer proper modal keyboard behavior.

The overlay does not trap focus, close on Escape, restore focus, or expose dialog semantics. Keyboard users can tab into obscured page content. Use the existing dialog/sheet primitive or implement equivalent focus management.

🤖 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 `@src/components/layout/Sidebar.tsx` around lines 240 - 263, Update the mobile
drawer rendered by the mobileOpen branch in SidebarContent usage to provide
modal dialog behavior: trap focus within the open drawer, close it on Escape,
restore focus to the triggering element after closing, and expose appropriate
dialog semantics. Prefer the existing dialog/sheet primitive if available;
otherwise implement equivalent focus management while preserving the current
onMobileClose behavior.

Comment on lines +26 to +31
<div
onClick={onOpen}
className={cn(
"group relative bg-elevated rounded-[16px] shadow-[var(--shadow-card)] p-[18px] flex flex-col gap-3 cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[var(--shadow-md)]",
!app.is_active && "opacity-70",
)}

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

Replace mouse-only card interactions with semantic, focus-visible controls. Both cards use non-focusable click handlers for their primary action and hide secondary actions until hover.

  • src/components/pages/apps/AppCard.tsx#L26-L31: use a link or button for opening the app.
  • src/components/pages/apps/AppCard.tsx#L77-L92: reveal edit/delete actions on keyboard focus and non-hover devices.
  • src/components/pages/projects/ProjectCard.tsx#L33-L44: make project navigation keyboard operable and reveal editing on focus.
📍 Affects 2 files
  • src/components/pages/apps/AppCard.tsx#L26-L31 (this comment)
  • src/components/pages/apps/AppCard.tsx#L77-L92
  • src/components/pages/projects/ProjectCard.tsx#L33-L44
🤖 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 `@src/components/pages/apps/AppCard.tsx` around lines 26 - 31, Replace the
non-focusable onClick container in AppCard with a semantic link or button for
opening the app, preserving its existing styling and action. In
src/components/pages/apps/AppCard.tsx lines 77-92, make edit/delete controls
visible on keyboard focus and devices without hover. In
src/components/pages/projects/ProjectCard.tsx lines 33-44, make project
navigation keyboard-operable and reveal the editing action on focus.

Comment on lines +65 to +72
<div className="space-y-2">
<Label>
Platforms <span className="font-normal text-fg-subtle">(minimum 1)</span>
</Label>
<PlatformMultiSelect
value={form.platforms}
onChange={(platforms) => setForm((f) => ({ ...f, platforms }))}
/>

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

Enforce the stated minimum platform count.

The save action remains enabled when form.platforms is empty, despite the “minimum 1” requirement.

Proposed fix
-<Button onClick={onSave} disabled={saving || !form.name.trim()}>
+<Button
+  onClick={onSave}
+  disabled={saving || !form.name.trim() || form.platforms.length === 0}
+>

Also applies to: 106-109

🤖 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 `@src/components/pages/apps/DetailsCard.tsx` around lines 65 - 72, Update the
save-action validation in the form containing the Platforms
`PlatformMultiSelect` so it is disabled whenever `form.platforms` is empty,
enforcing the displayed minimum of one platform. Preserve the existing save
behavior when at least one platform is selected, including the corresponding
validation path noted near the alternate save control.

Comment thread src/components/pages/apps/RolesCard.tsx Outdated
Comment on lines +70 to +90
<input
id="f_rl_name"
placeholder="e.g. Reviewer"
value={roleForm.label}
onChange={(e) => setRoleForm((f) => ({ ...f, label: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus:outline-none"
/>
</div>
<div className="flex-1 sm:max-w-[220px]">
<label
htmlFor="f_rl_key"
className="block text-[11px] font-semibold tracking-[0.08em] uppercase text-fg-subtle mb-1"
>
role_key
</label>
<input
id="f_rl_key"
placeholder="e.g. reviewer"
value={roleForm.role_key}
onChange={(e) => setRoleForm((f) => ({ ...f, role_key: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] font-mono text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus:outline-none"

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

Restore a visible keyboard focus indicator.

Both inputs remove their outline without providing a replacement. Prefer the shared Input, or add an explicit focus-visible ring.

Example fix
-className="w-full ... focus:outline-none"
+className="w-full ... focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
📝 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
<input
id="f_rl_name"
placeholder="e.g. Reviewer"
value={roleForm.label}
onChange={(e) => setRoleForm((f) => ({ ...f, label: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus:outline-none"
/>
</div>
<div className="flex-1 sm:max-w-[220px]">
<label
htmlFor="f_rl_key"
className="block text-[11px] font-semibold tracking-[0.08em] uppercase text-fg-subtle mb-1"
>
role_key
</label>
<input
id="f_rl_key"
placeholder="e.g. reviewer"
value={roleForm.role_key}
onChange={(e) => setRoleForm((f) => ({ ...f, role_key: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] font-mono text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus:outline-none"
<input
id="f_rl_name"
placeholder="e.g. Reviewer"
value={roleForm.label}
onChange={(e) => setRoleForm((f) => ({ ...f, label: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
/>
</div>
<div className="flex-1 sm:max-w-[220px]">
<label
htmlFor="f_rl_key"
className="block text-[11px] font-semibold tracking-[0.08em] uppercase text-fg-subtle mb-1"
>
role_key
</label>
<input
id="f_rl_key"
placeholder="e.g. reviewer"
value={roleForm.role_key}
onChange={(e) => setRoleForm((f) => ({ ...f, role_key: e.target.value }))}
className="w-full bg-elevated rounded-[10px] px-3 py-2 text-[13px] font-mono text-fg-strong placeholder:text-fg-subtle shadow-[var(--shadow-sm)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
🤖 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 `@src/components/pages/apps/RolesCard.tsx` around lines 70 - 90, Update the
role label and role_key inputs in RolesCard to provide a visible keyboard focus
indicator instead of only removing the outline. Prefer replacing them with the
shared Input component; otherwise add the established focus-visible ring styling
to both inputs while preserving their existing behavior and appearance.

Comment on lines +195 to +203
function handleRoleSelect(role: UserRole) {
if (!user || role === getUserRole(user)) return
if (role === "platform_admin") {
applyRoleUpdate({ role: "platform_admin" })
} else if (role === "manager") {
openManagerDialog()
} else {
setMemberConfirmOpen(true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require confirmation before granting platform-admin privileges.

Selecting platform_admin immediately submits the elevation, while lower-impact role changes require dialogs. An accidental click therefore grants unrestricted platform access. Add an explicit confirmation step.

🤖 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 `@src/components/pages/userDetail/index.tsx` around lines 195 - 203, Update
handleRoleSelect so selecting the platform_admin role opens an explicit
confirmation dialog instead of immediately calling applyRoleUpdate. Add or reuse
confirmation state and ensure the update is submitted only after the user
confirms, while preserving the existing manager and member confirmation flows.

Comment on lines 18 to 22
return (
<div
className={cn(card.interactive, "p-5 group")}
className={cn(card.interactive, "p-4 flex items-center gap-3")}
onClick={() => navigate(`/app/users/${user.id}`)}
>

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

Use a keyboard-accessible navigation element.

The clickable div cannot receive focus or activate via keyboard. Render the card as a Link or semantic button instead.

🤖 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 `@src/components/pages/UsersPage/UserCard.tsx` around lines 18 - 22, Update the
clickable card in UserCard to use a keyboard-accessible navigation element,
preferably a Link targeting `/app/users/${user.id}`, instead of the onClick-only
div. Preserve the existing styling and card content while removing the
non-semantic click navigation.

Comment thread src/components/ui/dialog.tsx Outdated
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-telha focus:ring-offset-2 disabled:pointer-events-none">
<DialogPrimitive.Close className="absolute right-4 top-4 w-8 h-8 rounded-[9px] grid place-items-center text-fg-subtle hover:bg-muted hover:text-fg-strong transition-colors focus:outline-none disabled:pointer-events-none">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing keyboard focus indicators after removing focus:outline-none. Both the dialog close button and the SelectTrigger remove the default focus outline without providing an alternative indicator (focus:ring-*, focus:border-*, etc.). Keyboard users get no visual feedback when tabbing to these controls.

  • src/components/ui/dialog.tsx#L41-L41: add a focus-visible:ring-2 focus-visible:ring-accent/40 (or equivalent) to the DialogPrimitive.Close className.
  • src/components/ui/select.tsx#L17-L17: add a focus-visible:ring-2 focus-visible:ring-accent/40 (or equivalent) to the SelectTrigger className.
📍 Affects 2 files
  • src/components/ui/dialog.tsx#L41-L41 (this comment)
  • src/components/ui/select.tsx#L17-L17
🤖 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 `@src/components/ui/dialog.tsx` at line 41, Restore visible keyboard focus
indicators on both DialogPrimitive.Close in src/components/ui/dialog.tsx:41-41
and SelectTrigger in src/components/ui/select.tsx:17-17 by adding equivalent
focus-visible ring styling, such as a 2px accent ring, alongside the existing
focus outline removal.

Comment thread src/index.css
Comment on lines 49 to +60
--font-sans: "Montserrat", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Merriweather", ui-serif, Georgia, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

:root {
--ease-out: cubic-bezier(0.2, 0.8, 0.25, 1);
--shell-dim: rgba(246, 245, 235, 0.62);
--shell-line: rgba(246, 245, 235, 0.13);
--shell-active: rgba(246, 245, 235, 0.12);
--edge: #6B6A52;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix the new Stylelint violations.

Lowercase the flagged font-family keywords, add an empty line before margin, and rename the keyframes to kebab-case with their references updated. The current declarations fail Stylelint.

Representative fix
-  --font-serif: "Merriweather", ui-serif, Georgia, serif;
-  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+  --font-serif: "Merriweather", ui-serif, georgia, serif;
+  --font-mono: ui-monospace, sfmono-regular, menlo, consolas, monospace;
...
-  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+  --mono: ui-monospace, sfmono-regular, menlo, consolas, monospace;
...
 body {
   `@apply` bg-canvas text-fg font-sans antialiased;
+
   margin: 0;
 }
...
-@keyframes tIn {
+@keyframes t-in {
...
-@keyframes fadeIn {
+@keyframes fade-in {
...
-@keyframes popIn {
+@keyframes pop-in {
...
-@keyframes pulseRing {
+@keyframes pulse-ring {
...
-.animate-fade-in { animation: fadeIn 0.16s ease-out }
+.animate-fade-in { animation: fade-in 0.16s ease-out }

Also applies to: 119-136

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 50-50: Expected "Georgia" to be "georgia" (value-keyword-case)

(value-keyword-case)


[error] 51-51: Expected "SFMono-Regular" to be "sfmono-regular" (value-keyword-case)

(value-keyword-case)


[error] 51-51: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 51-51: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)


[error] 60-60: Expected "SFMono-Regular" to be "sfmono-regular" (value-keyword-case)

(value-keyword-case)


[error] 60-60: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 60-60: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)

🤖 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 `@src/index.css` around lines 49 - 60, Update the CSS declarations in the
affected sections to satisfy Stylelint: lowercase the flagged font-family
keywords, insert the required empty line before margin declarations, and rename
the keyframes to kebab-case while updating every corresponding animation
reference. Preserve the existing styling and behavior.

Source: Linters/SAST tools

Comment thread src/stores/phasesStore.ts
Comment on lines +51 to +52
reset: () => {
set({ phases: [], dependencies: new Map(), loading: false, lastFetched: null })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Prevent in-flight fetches from undoing logout resets.

reset() clears the cache, but an already-running fetch() can subsequently repopulate it with the previous user’s phases and a fresh TTL. Abort outstanding requests or increment a generation token in reset(), then discard responses from older generations before calling set.

🤖 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 `@src/stores/phasesStore.ts` around lines 51 - 52, Update reset() in
phasesStore to invalidate any in-flight fetches by aborting their requests or
advancing a generation token. In fetch(), verify the request generation is still
current before applying phases, dependencies, loading state, or lastFetched via
set, discarding responses from older generations so logout cannot repopulate the
cache.

levigtri and others added 2 commits July 22, 2026 16:04
Platform admins land on the admin dashboard, so the Main nav item now
reads "Dashboard" for them; managers keep "My Apps".

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Platform admins get a restore action on inactive languages, calling
POST /languages/{id}/reactivate, and inactive rows now render on a
darker fill so Active vs Inactive reads clearly, per the design.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@levigtri

Copy link
Copy Markdown
Member Author

Review follow-ups (platform-admin fixes)

Pushed three fixes on top of the redesign:

  • Sidebar — the Main nav item now reads Dashboard for platform admins; managers keep My Apps.
  • Languages — inactive rows render on a darker fill so Active vs Inactive reads clearly (per the design).
  • Languages — platform admins get a restore action on inactive languages to reactivate them.

⚠️ Backend dependency: the reactivate action calls POST /api/languages/{id}/reactivate, a new endpoint added in tripod-api #121 (US-2.4 / OBT-264). That PR is stacked on the deactivate PR (tripod-api #97) and must be deployed for the restore button to work end to end.

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/components/pages/LanguagesPage.tsx (1)

69-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

refreshLanguages doesn't wait for the inactive-list reload, so the new reactivate/deactivate flows can show stale rows.

loadAllLanguages() is fired without await/return, so await refreshLanguages() in handleReactivate/handleDeactivate/handleSave only waits on fetchLanguages(). When showInactive is true, allLanguages (what actually renders the row) can still hold the pre-reactivation snapshot after the success toast fires and reactivatingId is cleared, letting an admin re-click "Reactivate" on a row that already succeeded before the UI catches up.

Proposed fix
-  function refreshLanguages() {
+  async function refreshLanguages() {
     useLanguagesStore.getState().invalidate()
-    if (showInactive) loadAllLanguages()
-    return fetchLanguages()
+    const [result] = await Promise.all([
+      fetchLanguages(),
+      showInactive ? loadAllLanguages() : Promise.resolve(),
+    ])
+    return result
   }
🤖 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 `@src/components/pages/LanguagesPage.tsx` around lines 69 - 73, Update
refreshLanguages so it waits for loadAllLanguages to complete when showInactive
is true, while preserving the existing fetchLanguages refresh and return
behavior. Ensure callers awaiting refreshLanguages do not continue until both
language lists are refreshed.
♻️ Duplicate comments (2)
src/components/pages/LanguagesPage.tsx (2)

221-221: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Table wrapper clips actions on narrow viewports.

overflow-hidden on the table container clips the Status/Actions columns instead of allowing horizontal scroll, so edit/deactivate/reactivate controls become unreachable on narrow screens.

Proposed fix
-      <div className="bg-elevated rounded-[18px] shadow-[var(--shadow-card)] overflow-hidden">
+      <div className="bg-elevated rounded-[18px] shadow-[var(--shadow-card)] overflow-x-auto">
🤖 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 `@src/components/pages/LanguagesPage.tsx` at line 221, Update the table wrapper
div in LanguagesPage to remove overflow-hidden and allow horizontal scrolling on
narrow viewports, preserving the existing rounded corners, elevated background,
and card shadow while keeping the Status and Actions controls reachable.

147-178: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Deactivate action stays enabled while stats are loading/failed, and can show mismatched project data.

openDeleteDialog fetches stats asynchronously but the dialog's Deactivate button (line 470) is only gated on deleting, not on whether deleteStats has loaded for the current deleteTarget. Selecting a different language before the previous stats request resolves can also let a stale response overwrite deleteStats for the wrong target, showing an incorrect project-impact message before confirmation.

Track a loading/error state and verify deleteStats?.language_id === deleteTarget?.id before enabling the destructive action.

Also applies to: 465-472

🤖 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 `@src/components/pages/LanguagesPage.tsx` around lines 147 - 178, Update
openDeleteDialog and the dialog’s Deactivate action to track stats loading/error
state, disable deactivation until stats successfully load for the current
deleteTarget, and require deleteStats?.language_id to match deleteTarget?.id.
Prevent stale asynchronous stats responses from updating deleteStats after the
selected target changes, preserving the existing handleDeactivate behavior once
validated stats are available.
🤖 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.

Outside diff comments:
In `@src/components/pages/LanguagesPage.tsx`:
- Around line 69-73: Update refreshLanguages so it waits for loadAllLanguages to
complete when showInactive is true, while preserving the existing fetchLanguages
refresh and return behavior. Ensure callers awaiting refreshLanguages do not
continue until both language lists are refreshed.

---

Duplicate comments:
In `@src/components/pages/LanguagesPage.tsx`:
- Line 221: Update the table wrapper div in LanguagesPage to remove
overflow-hidden and allow horizontal scrolling on narrow viewports, preserving
the existing rounded corners, elevated background, and card shadow while keeping
the Status and Actions controls reachable.
- Around line 147-178: Update openDeleteDialog and the dialog’s Deactivate
action to track stats loading/error state, disable deactivation until stats
successfully load for the current deleteTarget, and require
deleteStats?.language_id to match deleteTarget?.id. Prevent stale asynchronous
stats responses from updating deleteStats after the selected target changes,
preserving the existing handleDeactivate behavior once validated stats are
available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 412b6b88-d206-4580-9be8-9490854f765c

📥 Commits

Reviewing files that changed from the base of the PR and between c978612 and fd0be4f.

📒 Files selected for processing (3)
  • src/components/layout/Sidebar.tsx
  • src/components/pages/LanguagesPage.tsx
  • src/services/api.ts

levigtri and others added 5 commits July 22, 2026 18:07
… and icon

The project and app detail headers only exposed a bare, unlabeled tile
plus a lone Remove link, so there was no obvious way to add or change
the image. Both headers now show an accent Upload image/Upload icon (or
Change) link alongside Remove. The app icon editor moves to the header,
dropping the duplicate control from the app DetailsCard.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Rebuild the marker popup with Tailwind + semantic tokens: title, a
single location line, description, phase pills with the current phase
highlighted in telha, a compact meta line, and an accent Open project
button. Drops the inline hex styles and hand-drawn SVGs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a collapse toggle that shrinks the rail to a 72px icon-only mode
(icons + corner-badge counts + title tooltips, hairline section
dividers, single-icon theme toggle). State persists via a new
sidebarStore (Zustand + localStorage tc_sidebar). Mobile keeps the
full overlay drawer.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The logo, Public requests pill and Sign in link scrolled away with the
page. Make the header sticky top-0 with a canvas background.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Give ImageUpload an xl size plus upload/change/hint labels and use them
in the profile dialog for a cleaner, larger-photo layout. Add isolate
to the app <main> so body-portaled dialogs paint above the Leaflet map
(which sat at a higher z-index in the shared root stacking context).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@levigtri

Copy link
Copy Markdown
Member Author

UI fixes batch (platform-admin review round)

  • Upload affordance — project detail and manage-apps detail headers now show a labeled Upload image / Upload icon (→ Change) link next to Remove, so there's an obvious way to add/replace the image (previously only Remove was labeled). The app-icon editor moved to the header; the duplicate control was dropped from the app Details card.
  • Map popup — the marker popup was rebuilt on the Shema card design (title, location line, description, phase pills with the current phase in telha, meta line, accent Open project button).
  • Sidebar — added a collapsible desktop icon rail (72px, icons + corner-badge request counts + tooltips), toggle persisted via sidebarStore (localStorage tc_sidebar). Mobile keeps the full drawer.
  • Public request page — the top nav (logo, Public requests, Sign in) is now sticky on scroll.
  • Edit profile — larger avatar + cleaner layout, and the dialog now renders above the map (isolate on the app <main> walls off the Leaflet stacking context, so body-portaled Radix dialogs win).

⚠️ Backend dependency for the project cards: platform-admin accounts were being counted in project-card team size + avatars. The fix is in tripod-api (count_project_team_sizes + serialize_project_responses now filter is_platform_admin=False, with tests). It's applied to the local integration branch for testing; it still needs a backend PR against the projects serializer line (OBT-262 / OBT-216 lineage) before it ships.

@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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

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

⚠️ Outside diff range comments (2)
CLAUDE.md (1)

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

Align the sidebar example with role-specific labels.

Line 471 still documents “My Apps” as the default for all console users, but platform admins now see “Dashboard” while managers see “My Apps.” Update the sidebar mockup and role notes to match the implemented RBAC behavior.

🤖 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 `@CLAUDE.md` around lines 464 - 489, Update the Sidebar documentation mockup so
the Main item is labeled “Dashboard” for platform admins and “My Apps” for
managers, replacing the claim that “My Apps” is the default for all console
users. Revise the related role notes to explicitly describe this role-specific
landing behavior while preserving the existing Content and Administration
visibility rules.
src/components/pages/ProjectDetailPage.tsx (1)

1-11: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Import ChangeEvent before using it in the handler signature.
React.ChangeEvent has no local React binding in this file; import type ChangeEvent from react and update the parameter type.

🤖 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 `@src/components/pages/ProjectDetailPage.tsx` around lines 1 - 11, Update the
imports in ProjectDetailPage to include the type-only ChangeEvent symbol from
react, then change the handler’s parameter annotation from React.ChangeEvent to
ChangeEvent so it uses the available local type binding.
🟡 Other comments (1)
src/components/pages/map/ProjectPopupContent.tsx-59-81 (1)

59-81: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose each phase status as text, not only a colored dot.

Non-active chips render status exclusively via PHASE_DOT; add an accessible status label and hide the decorative dot from assistive technology.

Proposed fix
                 <span
+                  aria-hidden="true"
                   className={`h-1.5 w-1.5 flex-none rounded-full ${
                     active ? "bg-white/85" : PHASE_DOT[phase.status]
                   }`}
                 />
                 {phase.phase_name}
+                <span className="sr-only"> — {phase.status.replace("_", " ")}</span>
🤖 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 `@src/components/pages/map/ProjectPopupContent.tsx` around lines 59 - 81,
Update the phase chip rendering inside ProjectPopupContent’s phases.map callback
to expose phase.status as visible text for non-active phases, while preserving
the existing PHASE_DOT color indicator. Mark the colored dot as decorative for
assistive technology, and ensure the status text is accessible without changing
the active-chip styling.
🧹 Nitpick comments (1)
src/components/pages/AppDetailPage.tsx (1)

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

Extract the shared image upload flow
Both pages duplicate the same MIME/size checks and uploadsAPI.image call. Move that file-selection/upload logic into ImageUpload or a small shared hook, then let each page keep only its page-specific persistence (form.icon_url vs projectsAPI.update(...)).

🤖 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 `@src/components/pages/AppDetailPage.tsx` around lines 52 - 81, Extract the
duplicated image validation, upload, busy-state, error handling, and input-reset
logic from handleIconFile in src/components/pages/AppDetailPage.tsx (lines
52-81) and the corresponding handler in
src/components/pages/ProjectDetailPage.tsx (lines 104-129) into ImageUpload or a
shared hook. Keep each page responsible only for its persistence behavior:
updating form.icon_url in AppDetailPage and calling projectsAPI.update(...) in
ProjectDetailPage.
🤖 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.

Outside diff comments:
In `@CLAUDE.md`:
- Around line 464-489: Update the Sidebar documentation mockup so the Main item
is labeled “Dashboard” for platform admins and “My Apps” for managers, replacing
the claim that “My Apps” is the default for all console users. Revise the
related role notes to explicitly describe this role-specific landing behavior
while preserving the existing Content and Administration visibility rules.

In `@src/components/pages/ProjectDetailPage.tsx`:
- Around line 1-11: Update the imports in ProjectDetailPage to include the
type-only ChangeEvent symbol from react, then change the handler’s parameter
annotation from React.ChangeEvent to ChangeEvent so it uses the available local
type binding.

---

Other comments:
In `@src/components/pages/map/ProjectPopupContent.tsx`:
- Around line 59-81: Update the phase chip rendering inside
ProjectPopupContent’s phases.map callback to expose phase.status as visible text
for non-active phases, while preserving the existing PHASE_DOT color indicator.
Mark the colored dot as decorative for assistive technology, and ensure the
status text is accessible without changing the active-chip styling.

---

Nitpick comments:
In `@src/components/pages/AppDetailPage.tsx`:
- Around line 52-81: Extract the duplicated image validation, upload,
busy-state, error handling, and input-reset logic from handleIconFile in
src/components/pages/AppDetailPage.tsx (lines 52-81) and the corresponding
handler in src/components/pages/ProjectDetailPage.tsx (lines 104-129) into
ImageUpload or a shared hook. Keep each page responsible only for its
persistence behavior: updating form.icon_url in AppDetailPage and calling
projectsAPI.update(...) in ProjectDetailPage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: df2f60c7-82b5-4580-a286-df943e11be98

📥 Commits

Reviewing files that changed from the base of the PR and between fd0be4f and 3dd8483.

📒 Files selected for processing (11)
  • CLAUDE.md
  • src/components/common/ImageUpload.tsx
  • src/components/common/ProfileDialog.tsx
  • src/components/layout/AppShell.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/pages/AppDetailPage.tsx
  • src/components/pages/ProjectDetailPage.tsx
  • src/components/pages/apps/DetailsCard.tsx
  • src/components/pages/map/ProjectPopupContent.tsx
  • src/components/pages/publicRequest/index.tsx
  • src/stores/sidebarStore.ts
💤 Files with no reviewable changes (1)
  • src/components/pages/apps/DetailsCard.tsx

levigtri and others added 2 commits July 23, 2026 18:59
The popup only highlighted the in-progress phase, so completed, blocked
and not-started phases all rendered as the same grey chip and the card
looked unchanged when a status moved. Phase colors now come from a
shared PHASE_STATUS_CONFIG, reused by the project phases graph, and the
popup follows the design card: plain location line, tinted status chips
(capped at four plus a +N chip), no divider above the meta line and a
pill "Open project" button. The map also picks the dark CARTO tiles in
dark mode and keeps the popup close button.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…in use

The Projects column was a hardcoded dash, so there was no way to see how
many projects use a language. It now counts the projects visible to the
user, and the deactivate dialog matches the design: it is titled after
the language, seeds the project list from that count so it renders
without a flash, and when the language is in use it shows the "in use —
deactivation blocked" callout and disables the confirm button. A 409 from
the API is surfaced as its own toast.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

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

⚠️ Outside diff range comments (1)
src/components/pages/MapPage.tsx (1)

68-69: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not discard map projects when language loading fails.

Promise.all rejects when fetchLanguages() fails, so setProjects(projectsRes.data) is skipped and this no-op catch renders an empty map despite a successful /projects response. Load projects independently and treat missing language labels as optional.

🤖 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 `@src/components/pages/MapPage.tsx` around lines 68 - 69, Update the
map-loading flow in MapPage so a fetchLanguages failure does not prevent
successful project data from being passed to setProjects. Load the projects
response independently, treat language labels as optional when enriching
projects, and remove the no-op catch that discards the project list.
🟡 Other comments (1)
src/constants/phaseStatus.ts-35-40 (1)

35-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the blocked pill on the warning palette.

dot and ring use st-warn, but pill uses the accent palette. Use the shared warning tokens consistently so blocked phases do not appear as the brand accent.

Proposed fix
   blocked: {
     label: "Blocked",
-    pill: "bg-accent-soft text-on-accent-soft",
+    pill: "bg-st-warn/15 text-st-warn",
     dot: "bg-st-warn",
     ring: "ring-st-warn/40",
     icon: AlertTriangle,
🤖 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 `@src/constants/phaseStatus.ts` around lines 35 - 40, Update the blocked status
entry in phaseStatus to use the shared st-warn warning palette for pill,
matching its existing dot and ring tokens; leave the other status properties
unchanged.
🤖 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.

Outside diff comments:
In `@src/components/pages/MapPage.tsx`:
- Around line 68-69: Update the map-loading flow in MapPage so a fetchLanguages
failure does not prevent successful project data from being passed to
setProjects. Load the projects response independently, treat language labels as
optional when enriching projects, and remove the no-op catch that discards the
project list.

---

Other comments:
In `@src/constants/phaseStatus.ts`:
- Around line 35-40: Update the blocked status entry in phaseStatus to use the
shared st-warn warning palette for pill, matching its existing dot and ring
tokens; leave the other status properties unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 217e5ec9-8e44-484f-8308-5c4e7033d32c

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd8483 and d49405b.

📒 Files selected for processing (7)
  • src/components/pages/LanguagesPage.tsx
  • src/components/pages/MapPage.tsx
  • src/components/pages/ProjectPhasesTab.tsx
  • src/components/pages/map/FieldMapPanel.tsx
  • src/components/pages/map/ProjectPopupContent.tsx
  • src/constants/phaseStatus.ts
  • src/index.css

…ng its projects

Deactivation is a platform-admin action gated by the confirmation dialog,
not by usage: the API (tripod-api #97) allows it while projects still
reference the language. The dialog keeps listing those projects so the
admin decides with the usage in front of them, and the confirm button is
no longer disabled when the language is in use.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@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.

Caution

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

⚠️ Outside diff range comments (2)
src/components/pages/LanguagesPage.tsx (2)

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

Use displayLanguages for the empty-state check.

When no active languages exist, enabling “Include inactive” can populate allLanguages while languages.length stays zero. The page then continues rendering “No languages yet,” preventing platform admins from seeing or restoring inactive languages.

Proposed fix
-    languages.length === 0 ? (
+    displayLanguages.length === 0 ? (
🤖 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 `@src/components/pages/LanguagesPage.tsx` around lines 228 - 245, Update the
empty-state condition in the languages view to check displayLanguages.length
instead of languages.length. Keep displayLanguages as the source for determining
whether the table or EmptyState renders, so enabling showInactive exposes
inactive languages and preserves the existing empty-state behavior when none are
available.

56-61: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not convert project-load failures into zero project counts.

This request feeds every row’s project count and the deactivation fallback list. On failure, setProjects([]) makes an outage look like “no projects,” potentially hiding affected projects. Track the error and render an unavailable state or retain clearly marked last-known data instead.

🤖 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 `@src/components/pages/LanguagesPage.tsx` around lines 56 - 61, Update the
project-loading flow in LanguagesPage’s useEffect so a projectsAPI.list failure
is not converted into an empty projects array. Track the load error and render
project counts and the deactivation fallback using an unavailable state, or
retain clearly marked last-known projects, while preserving successful data
handling through setProjects.
🤖 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.

Outside diff comments:
In `@src/components/pages/LanguagesPage.tsx`:
- Around line 228-245: Update the empty-state condition in the languages view to
check displayLanguages.length instead of languages.length. Keep displayLanguages
as the source for determining whether the table or EmptyState renders, so
enabling showInactive exposes inactive languages and preserves the existing
empty-state behavior when none are available.
- Around line 56-61: Update the project-loading flow in LanguagesPage’s
useEffect so a projectsAPI.list failure is not converted into an empty projects
array. Track the load error and render project counts and the deactivation
fallback using an unavailable state, or retain clearly marked last-known
projects, while preserving successful data handling through setProjects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 6101fb01-8851-4f83-936c-3e7a18ef1656

📥 Commits

Reviewing files that changed from the base of the PR and between d49405b and 2cd2a70.

📒 Files selected for processing (1)
  • src/components/pages/LanguagesPage.tsx

levigtri and others added 6 commits July 23, 2026 19:37
Every arbitrary px length in the components and in the app stylesheet is now
expressed in rem, the root font-size is set to 125% and the Tailwind
breakpoints are scaled by the same factor so the layout still switches at the
same physical widths. The console at 100% browser zoom now matches the density
the design was reviewed at under 125% zoom.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The flow graph is an SVG laid out in fixed user units, so it does not follow
the rem-based UI scale. It now reads the scale from the root font-size and
applies it to the canvas height and to the fit-to-view zoom cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adopt the mock copy (Your profile / Save profile), switch the avatar
actions to inline Upload photo and Remove links, and drop the file-type
and size caption. The photo keeps the xl circle so it stays much larger
than the 52px avatar in the mock.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The link-action variant of ImageUpload now renders a plain circle with
no dashed ring and no hover overlay, and the profile dialog falls back
to the shared avatar initials and palette when there is no photo.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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