Skip to content

Refactor SPA into feature modules with enterprise contract#32

Merged
jruszo merged 2 commits intomasterfrom
feature/spa-feature-modules
Apr 20, 2026
Merged

Refactor SPA into feature modules with enterprise contract#32
jruszo merged 2 commits intomasterfrom
feature/spa-feature-modules

Conversation

@jruszo
Copy link
Copy Markdown
Owner

@jruszo jruszo commented Apr 20, 2026

Summary

  • Reorganize the Vue SPA into app, shared, and features layers with manifest-driven routes and navigation.
  • Extract shared auth, HTTP, access-control, and reusable UI/composable primitives.
  • Add an OSS enterprise extension stub and documented feature-module contract.
  • Migrate all community screens into feature-owned modules while preserving current routes and behavior.

Testing

  • npm run test:unit
  • npm run build
  • npm run e2e

Summary by CodeRabbit

  • New Features

    • Introduced modular feature architecture for composable routes and navigation
    • Added enterprise feature modules extension system for custom extensions
    • Implemented permission-based navigation and route access control
    • New application shell for unified layout and user management
  • UI Components

    • Added shared layout components (page headers, form actions, feedback alerts)
    • Enhanced settings infrastructure with new system settings UI framework
  • Tests

    • Added comprehensive test coverage for feature registry and access control

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Warning

Rate limit exceeded

@jruszo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 4 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 26 minutes and 4 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd352b34-9f6a-4601-8588-4a9decbbf067

📥 Commits

Reviewing files that changed from the base of the PR and between 3c95c2c and baa4c47.

📒 Files selected for processing (21)
  • frontend/package.json
  • frontend/src/app/AppShell.vue
  • frontend/src/app/feature-registry.ts
  • frontend/src/app/router.ts
  • frontend/src/features/archives/manifest.ts
  • frontend/src/features/permissions/manifest.ts
  • frontend/src/features/queries/components/index.ts
  • frontend/src/features/reports/pages/ReportsPage.vue
  • frontend/src/features/workflows/pages/ExportWorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/WorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue
  • frontend/src/shared/api/http-helpers.ts
  • frontend/src/shared/api/http.ts
  • frontend/src/shared/auth/access.test.ts
  • frontend/src/shared/auth/access.ts
  • frontend/src/shared/auth/api.ts
  • frontend/src/shared/auth/auth.ts
  • frontend/src/shared/components/feedback/FeedbackAlert.vue
  • frontend/src/shared/components/state/PageStateCard.vue
  • frontend/src/shared/composables/use-route-query-sync.ts
  • frontend/src/stores/auth.ts
📝 Walkthrough

Walkthrough

The pull request restructures the frontend application to use a feature module system. It introduces centralized router and authentication handling, moves shared utilities to a common location, creates feature-specific API surface files, and implements access control logic. Application shell responsibilities are extracted from App.vue into AppShell.vue. Each feature (auth, dashboard, archives, inventory, workflows, etc.) now declares routes and navigation via manifest files and exports from api.ts barrels.

Changes

Cohort / File(s) Summary
Feature Module Architecture
frontend/src/app/feature-contract.ts, frontend/src/app/feature-registry.ts, frontend/src/app/feature-registry.test.ts
Introduces FeatureModule type contract with routes and navigation. Feature registry aggregates modules from built-in manifests and enterprise extensions, providing utilities to collect routes, filter/sort navigation items by access, resolve settings navigation, and match navigation to routes.
Feature Manifests
frontend/src/features/auth/manifest.ts, frontend/src/features/dashboard/manifest.ts, frontend/src/features/archives/manifest.ts, frontend/src/features/inventory/manifest.ts, frontend/src/features/queries/manifest.ts, frontend/src/features/reports/manifest.ts, frontend/src/features/workflows/manifest.ts, frontend/src/features/permissions/manifest.ts, frontend/src/features/settings/manifest.ts
Each feature declares FeatureModule with routes (path, component, meta.title) and navigation entries (label, icon, section, order, access requirements). Routes include appropriate access gating; navigation items are sortable and permission-filtered.
Feature API Barrels
frontend/src/features/auth/api.ts, frontend/src/features/dashboard/api.ts, frontend/src/features/archives/api.ts, frontend/src/features/inventory/api.ts, frontend/src/features/queries/api.ts, frontend/src/features/workflows/api.ts, frontend/src/features/permissions/api.ts, frontend/src/features/settings/api.ts
New re-export modules providing feature-scoped import paths for functions and types from @/lib/api. Enables consumers to import feature-specific APIs from @/features/<feature>/api instead of the shared @/lib/api.
Feature Page Import Updates
frontend/src/features/*/pages/*.vue
Updated import paths for API functions/types from @/lib/api to relative ../api and component imports from @/components/queries/* to @/features/queries/components/*.
App Shell
frontend/src/app/AppShell.vue, frontend/src/App.vue
App.vue reduced to thin wrapper rendering AppShell. AppShell.vue contains sidebar, navigation, settings menu, header, user profile, and logout logic. Derives page title from route meta or navigation match, applies active route styling, manages auth initialization and token refresh, and handles logout with WorkOS redirect or router navigation.
Router
frontend/src/app/router.ts, frontend/src/router/index.ts
New centralized router.ts with routes from feature registry and beforeEach guard enforcing authentication (public routes bypass), authorizing access via canAccessRequirement, lazy-loading current user, handling settings route resolution, and clearing tokens on expiry. Legacy router/index.ts now re-exports from app/router.ts.
Auth Session Handling
frontend/src/app/install-auth-session.ts, frontend/src/lib/auth-session.ts
New install-auth-session.ts module installs one-time global event listeners for token updates and unauthorized access. Legacy auth-session.ts re-exports from app/install-auth-session.ts.
Shared Auth Infrastructure
frontend/src/shared/auth/auth.ts, frontend/src/shared/auth/access.ts, frontend/src/shared/auth/access.test.ts
New modules centralizing auth token management (storage, JWT expiry, refresh with request coalescing, event emission) and access control (permission checks, staff-admin gating, requirement evaluation). Tests validate permission filtering and access gate behavior.
Shared API Helpers
frontend/src/shared/api/http.ts
New centralized HTTP module with buildUrl, publicApiUrl, error flattening, and typed API methods (apiGet/Post/Patch/Put/Delete) supporting token auth, request deduplication, 401 handling with token refresh, and detailed error messages.
Shared Utilities & Components
frontend/src/shared/utils/cn.ts, frontend/src/shared/components/layout/PageHeader.vue, frontend/src/shared/components/layout/FormActions.vue, frontend/src/shared/components/feedback/FeedbackAlert.vue, frontend/src/shared/components/state/PageStateCard.vue, frontend/src/shared/composables/*
New shared utilities for class merging, layout components for page headers and form actions, feedback display, and composables for auth tokens, route query state syncing, user-facing error messages, and list state management.
Settings System
frontend/src/features/settings/system-settings.ts, frontend/src/lib/system-settings.ts
New settings system with field definitions, section groupings, default computation by input type, and settings initialization. Legacy lib/system-settings.ts re-exports from features/settings/system-settings.ts.
Import Consolidation
frontend/src/lib/api.ts, frontend/src/lib/auth.ts, frontend/src/lib/utils.ts
Legacy lib modules now re-export from shared/features modules, removing local implementations of HTTP helpers, auth token management, and class-merging utilities.
Configuration
frontend/tsconfig.app.json, frontend/vite.config.ts, frontend/package.json
Added @enterprise-feature-modules alias to Vite and TypeScript config for extensibility. Added vitest dev dependency and test:unit npm script for testing feature-registry and access utilities.
Documentation
frontend/docs/enterprise-feature-contract.md
New documentation explaining how OSS app composes feature modules from community and optional enterprise manifests, and how enterprise repos can inject modules via alias override.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser as Browser
    participant App as App.vue<br/>(thin wrapper)
    participant Shell as AppShell.vue<br/>(Layout)
    participant Router as Router<br/>(beforeEach guard)
    participant AuthStore as Auth Store<br/>(Pinia)
    participant SharedAuth as `@/shared/auth`<br/>(Token & Session)
    participant API as API<br/>(HTTP layer)
    participant FeatureRegistry as Feature<br/>Registry

    User->>Browser: Load app
    Browser->>App: Mount App.vue
    App->>Shell: Render AppShell
    Shell->>AuthStore: loadAuthConfig()
    AuthStore->>API: fetchAuthConfig()
    API-->>AuthStore: AuthMode, WorkOS URL, etc.
    AuthStore-->>Shell: Config loaded
    
    Shell->>SharedAuth: getUsableAccessToken()
    alt Token exists and valid
        SharedAuth-->>Shell: Return access token
    else Token missing or expired
        SharedAuth->>API: POST /auth/token/refresh/
        API-->>SharedAuth: New tokens
        SharedAuth->>SharedAuth: setStoredTokens()
        SharedAuth->>Browser: Emit AUTH_TOKENS_UPDATED_EVENT
        SharedAuth-->>Shell: Return new token
    end
    
    Shell->>AuthStore: loadCurrentUser()
    AuthStore->>API: fetchCurrentUserContext()
    API-->>AuthStore: User profile
    AuthStore-->>Shell: User loaded
    
    User->>Browser: Navigate to route
    Browser->>Router: beforeEach guard
    Router->>Router: Check auth (token valid?)
    alt Not authenticated
        Router->>AuthStore: clearTokens()
        Router->>Browser: Redirect /login
    else Authenticated
        Router->>Router: Check canAccessRequirement()
        alt Access denied
            Router->>Browser: Redirect /home
        else Access allowed
            Router->>FeatureRegistry: resolve() if /settings
            FeatureRegistry->>FeatureRegistry: getFirstVisibleSettingsItem()
            Router->>Browser: Allow navigation
        end
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

Hop along the feature trail,
Modules nest where routes prevail! 🐰
Auth guards gates with care so keen,
Shared utilities keep code clean.
From App to Shell, the flow divine,
Enterprise extensions intertwine! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Refactor SPA into feature modules with enterprise contract' accurately describes the primary architectural change: reorganizing the application into feature modules with an enterprise extension pattern.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/spa-feature-modules

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (10)
frontend/src/shared/components/state/PageStateCard.vue (1)

10-16: Consider exposing slots for richer state content.

As a shared "state card" meant for empty/error/loading contexts, consumers will typically want to render an icon, CTA button, or supplementary content. Currently the card is fixed to title + description only. Adding a default slot (and optionally a header/icon slot) would make it considerably more reusable without changing current call sites.

♻️ Proposed refactor
 <template>
   <Card class="border-slate-200">
     <CardHeader>
       <CardTitle>{{ title }}</CardTitle>
       <CardDescription>{{ description }}</CardDescription>
     </CardHeader>
+    <slot />
   </Card>
 </template>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/shared/components/state/PageStateCard.vue` around lines 10 - 16,
The PageStateCard component currently renders only CardHeader with CardTitle and
CardDescription from props (title, description); update it to expose a default
slot for richer body content and an optional named slot (e.g., "header" or
"icon") for custom header/icon rendering while preserving backward compatibility
by falling back to the existing CardTitle/CardDescription when those slots are
not provided; modify the template inside PageStateCard.vue to check for the
presence of the named slots and render them instead of the props, and keep props
title/description as the default content when slots are absent.
frontend/src/features/reports/pages/ReportsPage.vue (1)

14-23: Avoid duplicating the page title and description.

Line 14 adds a page-level header, but Lines 21-22 still repeat the same title and description inside the card. Consider making the card header section-specific so the page does not render duplicate header copy.

♻️ Proposed copy adjustment
     <Card>
       <CardHeader>
-        <CardTitle>Operational Reports</CardTitle>
-        <CardDescription>Platform-level overview for audit, review, and execution performance.</CardDescription>
+        <CardTitle>Performance metrics</CardTitle>
+        <CardDescription>Current approval, review, and query execution health.</CardDescription>
       </CardHeader>

As per coding guidelines, frontend/**/*.{tsx,jsx,vue}: Do not add hero topbars or oversized hero headers for internal product pages unless explicitly requested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/reports/pages/ReportsPage.vue` around lines 14 - 23,
The page currently renders duplicate header copy: the PageHeader component
(PageHeader title/description) and the CardHeader block
(CardTitle/CardDescription) both show the same "Operational Reports" title and
description; update the CardHeader inside ReportsPage.vue to be section-specific
(e.g., replace CardTitle/CardDescription with a contextual subtitle or remove
them) so only PageHeader contains the global page hero while CardHeader contains
a concise section label or is omitted for this card (locate PageHeader,
CardHeader, CardTitle, CardDescription in the file to implement the change).
frontend/package.json (1)

16-16: Consider using generic test discovery for future test files.

The current script lists only two specific test files. While all existing tests are included today, this hardcoded approach requires manual updates when new unit tests are added. Using vitest run without explicit paths ensures new tests are automatically discovered and run:

♻️ Proposed script update
-    "test:unit": "vitest run src/app/feature-registry.test.ts src/shared/auth/access.test.ts",
+    "test:unit": "vitest run",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` at line 16, The "test:unit" npm script currently
hardcodes two test file paths which will require manual updates; update the
"test:unit" script in package.json (the "test:unit" entry) to use vitest's
automatic discovery (e.g., run vitest without explicit file paths or with a
glob) so new unit tests are picked up automatically — replace the hardcoded
command with a generic vitest invocation like "vitest run" or "vitest run
<glob>" in the "test:unit" script.
frontend/src/stores/auth.ts (1)

4-4: Keep the global auth store off feature-scoped API barrels.

useAuthStore is foundational app state, so importing from @/features/auth/api makes shared auth state depend on a feature module. Prefer a shared/app auth API entrypoint and let the auth feature consume that same lower-level surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/stores/auth.ts` at line 4, The auth store is importing
feature-scoped API symbols (fetchAuthConfig, fetchCurrentUserContext, AuthMode,
CurrentUserContext) from the auth feature barrel which couples global state to a
feature; change the import to the shared/app-level auth API entrypoint (the
common auth API surface used by the rest of the app) so useAuthStore depends on
the core auth API instead of '@/features/auth/api', and update any re-exports if
necessary so the same symbols are imported from the shared auth entrypoint.
frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue (1)

6-6: Treat SqlCodeEditor as shared UI instead of a private queries component.

workflows now imports from features/queries/components/..., which couples two feature modules through an internal path. Since workflow pages also need this editor, consider moving it to shared or exposing it through a public queries component entrypoint.

Possible direction after extracting the shared editor
-import SqlCodeEditor from '@/features/queries/components/SqlCodeEditor.vue'
+import SqlCodeEditor from '@/shared/components/sql/SqlCodeEditor.vue'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue` at line 6,
The import of SqlCodeEditor in WorkflowSqlEditorPage.vue couples workflows to
queries' internal components; move the editor to a shared UI location or add a
public export from the queries feature and update imports. Specifically, extract
the component symbol SqlCodeEditor into a shared/components (or shared/ui)
module and update WorkflowSqlEditorPage.vue to import from the new shared path,
or alternatively add an index export (e.g., queries/components/index.ts) that
re-exports SqlCodeEditor and update the import to use that public entrypoint so
workflows no longer reference an internal path.
frontend/src/shared/auth/access.test.ts (1)

25-67: Consider adding a few more coverage cases.

Solid baseline tests. A couple of worthwhile additions when time permits:

  • requiresSuperuser: true path (covered by hasPermission but not directly via canAccessRequirement).
  • anyPermissions without any requiredPermissions (i.e., only the "any" branch matters).
  • Empty/undefined requirement object should return true (baseline behavior).
  • hasPermission for a non-superuser with a missing permission returning false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/shared/auth/access.test.ts` around lines 25 - 67, Add unit tests
covering the missing branches in access helpers: create tests that verify
canAccessRequirement with requiresSuperuser: true returns true for
buildUser({is_superuser: true}) and false otherwise; add a test where only
anyPermissions is provided (no requiredPermissions) and ensure
canAccessRequirement returns true when user has one of the anyPermissions and
false when they don't; add a test with an empty/undefined requirement object
(call canAccessRequirement(user, {}) or canAccessRequirement(user, undefined))
and assert it returns true; and add a negative hasPermission test asserting
hasPermission(buildUser({is_superuser: false}), 'some.missing_permission')
returns false. Reference the functions hasPermission, canAccessRequirement and
helper buildUser to locate where to add these cases.
frontend/src/app/AppShell.vue (1)

120-135: Verify logout path when loadAuthConfig() is slow/unreachable.

logout() awaits authStore.loadAuthConfig() before clearing tokens. If this network call hangs or is slow, logout is delayed and the user retains a valid session in the meantime. Consider clearing tokens first (optimistic local sign-out) and using a cached authMode to decide between WorkOS redirect and /login, or bounding the config load with a timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/AppShell.vue` around lines 120 - 135, In logout(), avoid
awaiting authStore.loadAuthConfig() before clearing tokens; instead perform an
immediate local sign-out by calling authStore.clearTokens() first, then attempt
to load or refresh auth config with a bounded timeout (or read a cached
authStore.authMode) to decide whether to redirect to
publicApiUrl('/auth/workos/logout/') or router.push('/login'); update the
control flow in the logout function to use the cached authMode or a timed
promise for loadAuthConfig() so a slow/unreachable network call cannot delay
clearing tokens or the redirect decision.
frontend/src/shared/api/http.ts (1)

58-141: 401 refresh/retry flow looks correct.

The auth gating (requiresAuth = options.token !== undefined), the single-retry guard via skipAuthRetry, and the separation between "refresh succeeded but server still 401s" (fires notifyUnauthorized and throws) vs. "refresh failed with AuthSessionExpiredError" (fires notifyUnauthorized and throws a formatted 401) are all handled cleanly. Callers that omit options.token get a pure unauthenticated request, which matches the publicApiUrl use case.

One small note worth being aware of: because requiresAuth is driven by the presence of options.token (even ""), every caller must pass some token value to get auth + retry behavior — that contract is implicit, so a brief comment above RequestOptions.token would help future feature-module authors avoid silently landing unauthenticated calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/shared/api/http.ts` around lines 58 - 141, Add a short comment
to the RequestOptions/InternalRequestOptions definition explaining that auth
gating uses the presence of options.token (even an empty string) to enable
authentication and the automatic refresh/retry flow; reference the options.token
field and the request function's requiresAuth check so future callers know they
must pass a token value to get auth+retry behavior and won't silently make
unauthenticated requests.
frontend/src/shared/auth/auth.ts (2)

14-48: Duplicate of helpers now in shared/api/http.ts.

isRecord, flattenErrorMessage, and parseResponseMessage here are near-identical to the ones exported from frontend/src/shared/api/http.ts (the http version additionally prefixes object keys). Consider importing the shared versions instead to keep error-message formatting consistent across the refresh path and the main request path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/shared/auth/auth.ts` around lines 14 - 48, This file defines
local duplicates of isRecord, flattenErrorMessage, and parseResponseMessage;
remove these local implementations and instead import and use the shared
implementations from the central HTTP helpers, replacing all references to the
local functions (isRecord, flattenErrorMessage, parseResponseMessage) with the
imported ones so error-message formatting is consistent across refresh and main
request paths. Ensure you import the exported names exactly (isRecord,
flattenErrorMessage, parseResponseMessage) and run a quick build/test to verify
no other local usages remain.

136-145: Client-side JWT validation of refresh token assumes backend uses JWTs.

isAccessTokenExpired(refreshToken) calls decodeJwtPayload, which requires a 3-segment JWT format with numeric exp claim (line 80–87). If the token isn't a valid JWT, it immediately returns true, triggering AuthSessionExpiredError without attempting the server refresh (line 139).

Backend verification confirms this is safe: SIMPLE_JWT configuration (archery/settings.py) with RefreshToken.for_user() (api_auth/views.py) always produces proper JWTs with exp claims. However, the architecture relies on an implicit client-side assumption. Consider letting the server validate and reject invalid refresh tokens (returning 401 from /auth/token/refresh/) rather than preemptively checking JWT format on the client.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/shared/auth/auth.ts` around lines 136 - 145, The client
currently treats the stored refresh token as a JWT by calling
isAccessTokenExpired(decodeJwtPayload) inside refreshAccessToken and throws
AuthSessionExpiredError when decodeJwtPayload fails; instead, change
refreshAccessToken to only check for presence of a refresh token (via
getStoredRefreshToken) and not call isAccessTokenExpired/decodeJwtPayload on the
refresh token so the server can validate format/expiry and return 401 if
invalid; keep the existing refreshRequest concurrency guard and ensure any
AuthSessionExpiredError is only thrown when there is no refresh token at all (or
after a server 401), referencing the refreshAccessToken, getStoredRefreshToken,
isAccessTokenExpired and AuthSessionExpiredError symbols to locate the spots to
remove the preemptive JWT validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/app/AppShell.vue`:
- Around line 192-210: The Settings toggle button lacks ARIA state and control
linkage; update the button (the element using isSettingsRouteActive,
toggleSettingsMenu, isSidebarCollapsed and isSettingsMenuOpen) to include
aria-expanded bound to isSettingsMenuOpen and aria-controls referencing the id
of the submenu region; ensure the submenu element (the sibling that is
shown/hidden by isSettingsMenuOpen) has a matching unique id (e.g.,
settings-submenu or generated id) and appropriate role (region or menu) so
assistive tech can discover and announce the expanded/collapsed state and the
controlled region.

In `@frontend/src/app/feature-registry.ts`:
- Around line 55-66: The current matchesNavigationItem function uses
currentPath.startsWith(targetPath) which can incorrectly mark sibling routes as
active (e.g., "/workflow" matching "/workflows"); update the logic in
matchesNavigationItem to treat a prefix match as valid only when currentPath
equals targetPath or when currentPath starts with targetPath followed by a '/'
(i.e., check currentPath === targetPath || currentPath.startsWith(targetPath +
'/')), keep the existing special-case for '/' and continue deriving targetPath
from item.matchPrefix ?? item.to to locate the code to change.

In `@frontend/src/app/router.ts`:
- Around line 63-90: The catch blocks around ensureCurrentUser() (which calls
authStore.loadCurrentUser()) are currently treating any error as session expiry;
change them to only clear tokens and redirect to the login-expired flow when the
error is an AuthSessionExpiredError or an HTTP 401 response from
loadCurrentUser(), otherwise rethrow or navigate to a generic error route.
Concretely, update both try/catch blocks that call ensureCurrentUser() (used by
the settings branch that uses getFirstVisibleSettingsItem() and the meta.access
branch that uses canAccessRequirement()) so the catch inspects the error (e.g.,
instanceof AuthSessionExpiredError or error.status === 401) before calling
clearStoredTokens() and authStore.clearTokens() and returning { name: 'login',
query: { reason: 'expired' } }; for non-expiry errors either rethrow the error
or return a generic error navigation (e.g., { name: 'error' }) so
transient/network/server errors don't log users out.

In `@frontend/src/features/archives/manifest.ts`:
- Around line 11-22: The three route objects for Archives (route names
'archives', 'archive-new', 'archive-detail' with components ArchivesPage,
ArchiveCreatePage, ArchiveDetailPage) lack route-level access metadata while the
navigation item is gated by sql.menu_archive; add meta.access to each route
matching the nav guard (e.g., meta: { title: '...', access: { anyPermissions:
['sql.menu_archive'] } }) so the router authorization prevents direct access to
/archives, /archives/new, and /archives/:archiveId for users without the menu
permission.

In `@frontend/src/features/permissions/manifest.ts`:
- Around line 9-18: The route for '/permission-management' is unprotected while
the navigation item has an access guard; add the same access requirement to the
route's meta so direct URL access is guarded. Update the route entry that maps
path '/permission-management' / component PermissionManagementPage to include
meta.access with the same object used in navigation (anyPermissions:
['sql.menu_queryapplylist']), ensuring the route's meta contains title and the
new access field.

In `@frontend/src/features/settings/manifest.ts`:
- Line 16: The /settings landing route currently defined as { path: '/settings',
name: 'settings', component: SettingsLandingPage, meta: { title: 'Settings' } }
exposes the settings index without any access gate; either add an access
requirement to this route consistent with the sub-pages (e.g., same
staff/superuser/permission checks used by those sub-routes) or change
SettingsLandingPage to immediately redirect to the first visible/authorized
settings nav entry resolved by your registry logic; locate the route definition
(path '/settings', component SettingsLandingPage) and implement one of these two
fixes so users cannot land on an inaccessible overview page.

In `@frontend/src/shared/auth/access.ts`:
- Around line 11-20: hasPermission currently assumes currentUser.permissions
exists and calls .includes, which can throw if permissions is null/undefined;
update hasPermission to first check currentUser?.is_superuser, then verify that
currentUser.permissions is an array (e.g.,
Array.isArray(currentUser.permissions)) before calling .includes, and return
false if permissions is missing or not an array so callers (like
canAccessRequirement) won't crash; keep the function name hasPermission and
existing return behavior for superusers.

In `@frontend/src/shared/components/feedback/FeedbackAlert.vue`:
- Around line 2-8: hasError and hasSuccess are plain booleans computed once from
defineProps and won't update when props change; make them reactive by replacing
the current const hasError and const hasSuccess with Vue computed properties
(e.g., use computed(() => Boolean(props.error)) and computed(() =>
!hasError.value && Boolean(props.success))) or by inlining Boolean(props.error)
/ Boolean(props.success) checks in the template so the alert updates when
props.error or props.success change; update references to use .value if you
choose computed.

In `@frontend/src/shared/composables/use-route-query-sync.ts`:
- Around line 26-33: routeQueriesMatch currently compares compacted queries
using JSON.stringify which can differ when object key orders vary; to fix,
normalize key order before comparing by deep-sorting object keys (e.g.,
implement a small helper like deepSortKeys and call it on normalizedRouteQuery
and normalizedCurrentQuery) and then compare the results with JSON.stringify (or
use a stable stringify that sorts keys). Update routeQueriesMatch to use
compactRouteQuery, then deepSortKeys(compactResult) for both inputs (or reuse a
stable serializer) so equivalent queries with different insertion orders compare
equal.

---

Nitpick comments:
In `@frontend/package.json`:
- Line 16: The "test:unit" npm script currently hardcodes two test file paths
which will require manual updates; update the "test:unit" script in package.json
(the "test:unit" entry) to use vitest's automatic discovery (e.g., run vitest
without explicit file paths or with a glob) so new unit tests are picked up
automatically — replace the hardcoded command with a generic vitest invocation
like "vitest run" or "vitest run <glob>" in the "test:unit" script.

In `@frontend/src/app/AppShell.vue`:
- Around line 120-135: In logout(), avoid awaiting authStore.loadAuthConfig()
before clearing tokens; instead perform an immediate local sign-out by calling
authStore.clearTokens() first, then attempt to load or refresh auth config with
a bounded timeout (or read a cached authStore.authMode) to decide whether to
redirect to publicApiUrl('/auth/workos/logout/') or router.push('/login');
update the control flow in the logout function to use the cached authMode or a
timed promise for loadAuthConfig() so a slow/unreachable network call cannot
delay clearing tokens or the redirect decision.

In `@frontend/src/features/reports/pages/ReportsPage.vue`:
- Around line 14-23: The page currently renders duplicate header copy: the
PageHeader component (PageHeader title/description) and the CardHeader block
(CardTitle/CardDescription) both show the same "Operational Reports" title and
description; update the CardHeader inside ReportsPage.vue to be section-specific
(e.g., replace CardTitle/CardDescription with a contextual subtitle or remove
them) so only PageHeader contains the global page hero while CardHeader contains
a concise section label or is omitted for this card (locate PageHeader,
CardHeader, CardTitle, CardDescription in the file to implement the change).

In `@frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue`:
- Line 6: The import of SqlCodeEditor in WorkflowSqlEditorPage.vue couples
workflows to queries' internal components; move the editor to a shared UI
location or add a public export from the queries feature and update imports.
Specifically, extract the component symbol SqlCodeEditor into a
shared/components (or shared/ui) module and update WorkflowSqlEditorPage.vue to
import from the new shared path, or alternatively add an index export (e.g.,
queries/components/index.ts) that re-exports SqlCodeEditor and update the import
to use that public entrypoint so workflows no longer reference an internal path.

In `@frontend/src/shared/api/http.ts`:
- Around line 58-141: Add a short comment to the
RequestOptions/InternalRequestOptions definition explaining that auth gating
uses the presence of options.token (even an empty string) to enable
authentication and the automatic refresh/retry flow; reference the options.token
field and the request function's requiresAuth check so future callers know they
must pass a token value to get auth+retry behavior and won't silently make
unauthenticated requests.

In `@frontend/src/shared/auth/access.test.ts`:
- Around line 25-67: Add unit tests covering the missing branches in access
helpers: create tests that verify canAccessRequirement with requiresSuperuser:
true returns true for buildUser({is_superuser: true}) and false otherwise; add a
test where only anyPermissions is provided (no requiredPermissions) and ensure
canAccessRequirement returns true when user has one of the anyPermissions and
false when they don't; add a test with an empty/undefined requirement object
(call canAccessRequirement(user, {}) or canAccessRequirement(user, undefined))
and assert it returns true; and add a negative hasPermission test asserting
hasPermission(buildUser({is_superuser: false}), 'some.missing_permission')
returns false. Reference the functions hasPermission, canAccessRequirement and
helper buildUser to locate where to add these cases.

In `@frontend/src/shared/auth/auth.ts`:
- Around line 14-48: This file defines local duplicates of isRecord,
flattenErrorMessage, and parseResponseMessage; remove these local
implementations and instead import and use the shared implementations from the
central HTTP helpers, replacing all references to the local functions (isRecord,
flattenErrorMessage, parseResponseMessage) with the imported ones so
error-message formatting is consistent across refresh and main request paths.
Ensure you import the exported names exactly (isRecord, flattenErrorMessage,
parseResponseMessage) and run a quick build/test to verify no other local usages
remain.
- Around line 136-145: The client currently treats the stored refresh token as a
JWT by calling isAccessTokenExpired(decodeJwtPayload) inside refreshAccessToken
and throws AuthSessionExpiredError when decodeJwtPayload fails; instead, change
refreshAccessToken to only check for presence of a refresh token (via
getStoredRefreshToken) and not call isAccessTokenExpired/decodeJwtPayload on the
refresh token so the server can validate format/expiry and return 401 if
invalid; keep the existing refreshRequest concurrency guard and ensure any
AuthSessionExpiredError is only thrown when there is no refresh token at all (or
after a server 401), referencing the refreshAccessToken, getStoredRefreshToken,
isAccessTokenExpired and AuthSessionExpiredError symbols to locate the spots to
remove the preemptive JWT validation.

In `@frontend/src/shared/components/state/PageStateCard.vue`:
- Around line 10-16: The PageStateCard component currently renders only
CardHeader with CardTitle and CardDescription from props (title, description);
update it to expose a default slot for richer body content and an optional named
slot (e.g., "header" or "icon") for custom header/icon rendering while
preserving backward compatibility by falling back to the existing
CardTitle/CardDescription when those slots are not provided; modify the template
inside PageStateCard.vue to check for the presence of the named slots and render
them instead of the props, and keep props title/description as the default
content when slots are absent.

In `@frontend/src/stores/auth.ts`:
- Line 4: The auth store is importing feature-scoped API symbols
(fetchAuthConfig, fetchCurrentUserContext, AuthMode, CurrentUserContext) from
the auth feature barrel which couples global state to a feature; change the
import to the shared/app-level auth API entrypoint (the common auth API surface
used by the rest of the app) so useAuthStore depends on the core auth API
instead of '@/features/auth/api', and update any re-exports if necessary so the
same symbols are imported from the shared auth entrypoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94219d8f-2b44-4046-8e76-71a8d26539aa

📥 Commits

Reviewing files that changed from the base of the PR and between d30dfae and 3c95c2c.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (83)
  • frontend/docs/enterprise-feature-contract.md
  • frontend/package.json
  • frontend/src/App.vue
  • frontend/src/app/AppShell.vue
  • frontend/src/app/feature-contract.ts
  • frontend/src/app/feature-registry.test.ts
  • frontend/src/app/feature-registry.ts
  • frontend/src/app/install-auth-session.ts
  • frontend/src/app/router.ts
  • frontend/src/extensions/enterprise-feature-modules.ts
  • frontend/src/features/archives/api.ts
  • frontend/src/features/archives/manifest.ts
  • frontend/src/features/archives/pages/ArchiveCreatePage.vue
  • frontend/src/features/archives/pages/ArchiveDetailPage.vue
  • frontend/src/features/archives/pages/ArchivesPage.vue
  • frontend/src/features/auth/api.ts
  • frontend/src/features/auth/manifest.ts
  • frontend/src/features/auth/pages/LoginCallbackPage.vue
  • frontend/src/features/auth/pages/LoginPage.vue
  • frontend/src/features/auth/pages/ProfilePage.vue
  • frontend/src/features/dashboard/api.ts
  • frontend/src/features/dashboard/manifest.ts
  • frontend/src/features/dashboard/pages/HomePage.vue
  • frontend/src/features/inventory/api.ts
  • frontend/src/features/inventory/manifest.ts
  • frontend/src/features/inventory/pages/InventoryEditorPage.vue
  • frontend/src/features/inventory/pages/InventoryListPage.vue
  • frontend/src/features/permissions/api.ts
  • frontend/src/features/permissions/manifest.ts
  • frontend/src/features/permissions/pages/PermissionManagementPage.vue
  • frontend/src/features/queries/api.ts
  • frontend/src/features/queries/components/QueryMetadataExplorer.vue
  • frontend/src/features/queries/components/SqlCodeEditor.vue
  • frontend/src/features/queries/components/query-metadata-explorer.ts
  • frontend/src/features/queries/manifest.ts
  • frontend/src/features/queries/pages/QueriesPage.vue
  • frontend/src/features/reports/manifest.ts
  • frontend/src/features/reports/pages/ReportsPage.vue
  • frontend/src/features/settings/api.ts
  • frontend/src/features/settings/manifest.ts
  • frontend/src/features/settings/pages/SettingsGroupDetailPage.vue
  • frontend/src/features/settings/pages/SettingsGroupsPage.vue
  • frontend/src/features/settings/pages/SettingsInstanceTagDetailPage.vue
  • frontend/src/features/settings/pages/SettingsInstanceTagsPage.vue
  • frontend/src/features/settings/pages/SettingsLandingPage.vue
  • frontend/src/features/settings/pages/SettingsResourceGroupDetailPage.vue
  • frontend/src/features/settings/pages/SettingsResourceGroupsPage.vue
  • frontend/src/features/settings/pages/SettingsSystemPage.vue
  • frontend/src/features/settings/pages/SettingsUserDetailPage.vue
  • frontend/src/features/settings/pages/SettingsUsersPage.vue
  • frontend/src/features/settings/system-settings.ts
  • frontend/src/features/workflows/api.ts
  • frontend/src/features/workflows/manifest.ts
  • frontend/src/features/workflows/pages/DdlWorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/DmlWorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/ExportWorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/WorkflowCreatePage.vue
  • frontend/src/features/workflows/pages/WorkflowDetailPage.vue
  • frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue
  • frontend/src/features/workflows/pages/WorkflowsPage.vue
  • frontend/src/lib/api.ts
  • frontend/src/lib/auth-session.ts
  • frontend/src/lib/auth.ts
  • frontend/src/lib/system-settings.ts
  • frontend/src/lib/utils.ts
  • frontend/src/main.ts
  • frontend/src/router/index.ts
  • frontend/src/shared/api/http.ts
  • frontend/src/shared/auth/access.test.ts
  • frontend/src/shared/auth/access.ts
  • frontend/src/shared/auth/auth.ts
  • frontend/src/shared/components/feedback/FeedbackAlert.vue
  • frontend/src/shared/components/layout/FormActions.vue
  • frontend/src/shared/components/layout/PageHeader.vue
  • frontend/src/shared/components/state/PageStateCard.vue
  • frontend/src/shared/composables/use-auth-token.ts
  • frontend/src/shared/composables/use-list-query-state.ts
  • frontend/src/shared/composables/use-route-query-sync.ts
  • frontend/src/shared/composables/use-user-facing-message.ts
  • frontend/src/shared/utils/cn.ts
  • frontend/src/stores/auth.ts
  • frontend/tsconfig.app.json
  • frontend/vite.config.ts

Comment thread frontend/src/app/AppShell.vue
Comment thread frontend/src/app/feature-registry.ts
Comment thread frontend/src/app/router.ts
Comment thread frontend/src/features/archives/manifest.ts Outdated
Comment thread frontend/src/features/permissions/manifest.ts Outdated
Comment thread frontend/src/features/settings/manifest.ts
Comment thread frontend/src/shared/auth/access.ts
Comment thread frontend/src/shared/components/feedback/FeedbackAlert.vue Outdated
Comment thread frontend/src/shared/composables/use-route-query-sync.ts
@jruszo jruszo merged commit 09dc498 into master Apr 20, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant