Skip to content

Nav editor#1630

Merged
j-paterson merged 35 commits intocanaryfrom
navEditor
Jan 15, 2026
Merged

Nav editor#1630
j-paterson merged 35 commits intocanaryfrom
navEditor

Conversation

@j-paterson
Copy link
Copy Markdown
Collaborator

@j-paterson j-paterson commented Dec 19, 2025

Summary by CodeRabbit

  • New Features
    • Full admin navigation editor: drag‑drop, inline rename, icon picker, create/delete, commit/cancel; local‑first navigation store and client rebuild when server data missing.
  • API
    • PUT endpoint to update community navigation config with validation, signing, and cleanup.
  • Bug Fixes
    • Improved space fallback/reconstruction and safer redirect to first nav item.
  • Documentation
    • Comprehensive Navigation System docs; updated getting-started env var example.
  • Refactors
    • Utilities and stores hardened for robustness.
  • Tests
    • Navigation error‑handling tests added.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
space-system Ready Ready Preview, Comment Jan 15, 2026 7:12pm

@j-paterson j-paterson marked this pull request as draft December 19, 2025 22:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 19, 2025

Warning

Rate limit exceeded

@j-paterson has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 40 seconds before requesting another review.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d5865cf and 6783efb.

📒 Files selected for processing (1)
  • src/common/components/organisms/Navigation.tsx
📝 Walkthrough

Walkthrough

Adds a local-first, admin-editable navigation system: new Zustand navigation store and hooks, drag-and-drop editor and item components, signed server API for navigation config updates with validation, SystemConfig extensions (communityId, adminIdentityPublicKeys), client-side fallback for missing server space data, plus related docs and tests.

Changes

Cohort / File(s) Summary
Navigation Store & Actions
src/common/data/stores/app/navigation/navigationStore.ts, src/common/data/stores/app/index.tsx
New navigation store slice: remote/local navigation, hrefMappings, CRUD, reorder, multi-step commit with signed space registration, reset/hasUncommittedChanges. AppStore integrated.
Navigation API & Server Validation
src/pages/api/navigation/config.ts
New PUT handler that verifies signed requests, enforces timestamp replay protection, checks admin rights, validates items, updates DB navigation_config, and cleans up deleted artifacts.
Editor UI & Components
src/common/components/organisms/navigation/NavigationEditor.tsx, src/common/components/organisms/navigation/NavigationItem.tsx, src/common/components/organisms/NavigationManagement.tsx, src/common/components/organisms/navigation/ErrorBoundary.tsx
New NavigationEditor/NavigationItem components, management UI, and an error boundary; drag-and-drop reordering, inline editing, icon selection, create/delete flows.
Navigation Hook & Utilities
src/common/components/organisms/navigation/useNavigation.ts, src/common/components/organisms/navigation/errorHandling.ts, src/common/components/organisms/navigation/constants.ts, src/common/utils/navUtils.ts, src/common/utils/tabUtils.ts
New useNavigation hook with debounced reorder/commit/cancel/create flows; normalized NavigationError handling; validation and unique-name utilities and constants.
Top-level Integration & Sidebar
src/common/components/organisms/Navigation.tsx, src/common/components/organisms/Sidebar.tsx
Navigation gains admin edit mode, editor integration and icon-pack aggregation; Sidebar exposes navEditMode/setNavEditMode and toggles behavior during edit mode.
Page-level Client Fallbacks
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx, src/app/[navSlug]/[[...tabName]]/page.tsx, src/app/page.tsx
Server can return null spaceData and passes adminIdentityPublicKeys; client reconstructs space data from local stores (local-first) or shows 404; home redirect simplified to first nav item.
Atoms & UI Adjustments
src/common/components/atoms/editable-text.tsx, src/common/components/atoms/reorderable-tab.tsx, src/common/components/organisms/TabBar.tsx, src/common/lib/theme/ThemeSettingsEditor.tsx
EditableText: validation, maxLength, sync; reorderable-tab conditional rename gating; TabBar uses generateUniqueName; theme editor respects nav edit mode.
Space Store Robustness
src/common/data/stores/app/space/spaceStore.ts
Improved load/commit paths: initialization guards for remoteSpaces, robust JSON/html error handling, and sync fixes during commits.
Config & SystemConfig Changes
src/config/systemConfig.ts, src/config/loaders/registry.ts, src/config/index.ts
SystemConfig adds communityId and adminIdentityPublicKeys; NavigationItem.icon widened to string; registry transform now includes communityId; cache cleanup and domain-warning deduplication added.
Global Providers & Error Handling
src/common/providers/GlobalErrorHandler.tsx, src/common/providers/index.tsx
New GlobalErrorHandler suppresses noisy browser-extension errors in dev; mounted at provider root.
Docs, Tests & Misc
docs/SYSTEMS/NAVIGATION/OVERVIEW.md, docs/GETTING_STARTED.md, docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md, tests/navigation/errorHandling.test.ts, scripts/seed.ts
Navigation docs added/updated; tests for navigation error handling; seed/docs environment variable renamed SUPABASE_SERVICE_ROLE_KEYSUPABASE_SERVICE_KEY.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin (Browser)
    participant Editor as NavigationEditor
    participant Store as Navigation Store
    participant SpaceSvc as Space Service
    participant API as /api/navigation/config
    participant DB as Database

    Admin->>Editor: Enter edit mode and modify items
    Editor->>Store: create/update/delete/reorder (local state)
    Admin->>Editor: Commit
    Editor->>Store: commitNavigationChanges()
    Store->>SpaceSvc: Register new spaces (signed requests)
    SpaceSvc->>DB: Create space records
    SpaceSvc-->>Store: Return spaceIds
    Store->>API: PUT /api/navigation/config (signed payload)
    API->>DB: Verify admin, validate items, update navigation_config
    DB-->>API: Success
    API-->>Store: Confirm update
    Store->>Editor: Update remoteNavigation, clear uncommitted changes
    Editor->>Admin: Show success, exit edit mode
Loading
sequenceDiagram
    participant User as Browser
    participant Page as page.tsx (server)
    participant Client as NavPageSpace (client)
    participant LocalStore as Local Store
    participant DB as Database

    User->>Page: Request /[navSlug]/[[...tabName]]
    Page->>DB: Fetch spaceData for navSlug
    alt spaceData found
        Page-->>Client: spaceData (server-rendered)
        Client->>Client: Render from server data
    else spaceData missing
        Page-->>Client: null + adminIdentityPublicKeys
        Client->>LocalStore: Query localNavigation & local spaces
        alt Local data available
            LocalStore-->>Client: Construct spaceData from local
            Client->>User: Render local-first space
        else
            Client-->>User: Render 404
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

codex, LGFTP

Poem

🐰 I hop through nav with nimble paws,
I drag and name without long laws,
Local-first I stitch each page,
Signed commits calm the admin stage,
Hooray — the rabbit tweaks the maze! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Nav editor' is vague and non-descriptive; it uses generic terminology that does not convey the scope or main objectives of the substantial changeset, which includes navigation editing, store infrastructure, API endpoints, and client-side UI components. Expand the title to be more specific and descriptive of the main change, such as 'Add navigation editor with local-first data resolution and API persistence' or similar, to help reviewers quickly understand the primary objectives.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

Caution

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

⚠️ Outside diff range comments (1)
src/common/components/organisms/Sidebar.tsx (1)

63-81: navEditMode is not destructured, causing incorrect conditional logic.

On line 79, navEditMode is referenced but it's not included in the destructuring on line 64. This means navEditMode will be undefined, and the condition editMode && !navEditMode will effectively be editMode && true, which defeats the purpose of the navigation edit mode check.

🔎 Proposed fix
-  const { editMode, setEditMode, sidebarEditable, portalRef } =
+  const { editMode, setEditMode, navEditMode, sidebarEditable, portalRef } =
     useSidebarContext();
🧹 Nitpick comments (10)
src/common/data/stores/app/index.tsx (1)

70-78: Consider clearing navigation state on logout.

The logout function resets account, homebase, space, checkpoints, and chat stores, but doesn't reset navigation state. If users make local navigation edits, those could persist after logout and potentially be visible to another user on the same device.

🔎 Proposed fix
   logout: () => {
     get().account.reset();
     get().homebase.clearHomebase();
     get().homebase.clearHomebaseTabOrder();
     get().space.clear();
     get().checkpoints.clearCheckpoints();
     get().chat.clearMessages();
+    get().navigation.resetNavigationChanges();
     localStorage.removeItem(LOCAL_STORAGE_LOCATION);
   },
src/common/components/organisms/NavigationManagement.tsx (3)

54-56: Simplify the nested ternary logic for readability.

The current logic is hard to follow with nested ternaries. Consider simplifying:

🔎 Proposed refactor
-  // Use local navigation items if available, otherwise fall back to props
-  // Only use localNavigation if it has items (not empty array from initial state)
-  const items = localNavigation.length > 0 || navigationItems.length === 0
-    ? localNavigation.length > 0 ? localNavigation : navigationItems
-    : navigationItems;
+  // Use local navigation items if available, otherwise fall back to props
+  const items = localNavigation.length > 0 ? localNavigation : navigationItems;

The simplified version achieves the same result: use localNavigation when it has items, otherwise use navigationItems.


58-63: Add cleanup for the debounced function.

The debounced function should be cancelled on unmount to prevent potential memory leaks or state updates on unmounted components.

🔎 Proposed fix using useMemo with cleanup
+import { useEffect, useMemo } from "react";
+
-  const debouncedUpdateOrder = useCallback(
-    debounce((newOrder: NavigationItem[]) => {
+  const debouncedUpdateOrder = useMemo(
+    () => debounce((newOrder: NavigationItem[]) => {
       updateNavigationOrder(newOrder);
     }, 300),
     [updateNavigationOrder]
   );
+
+  useEffect(() => {
+    return () => {
+      debouncedUpdateOrder.cancel();
+    };
+  }, [debouncedUpdateOrder]);

107-115: Consider removing the 404-as-success fallback before production.

Treating 404 errors as success is useful for local testing, but this should be removed or feature-flagged before production deployment to avoid masking real API issues.

🔎 Suggestion

Consider wrapping this logic in an environment check:

     } catch (error: any) {
       console.error("Failed to commit navigation changes:", error);
-      // For local testing, still show success if it's just a 404 (API not implemented)
-      if (error?.response?.status === 404) {
+      // TODO: Remove this fallback before production
+      if (process.env.NODE_ENV === 'development' && error?.response?.status === 404) {
         toast.success("Navigation changes staged locally (API endpoints not yet implemented)");
         onCommit?.();
       } else {
         toast.error("Failed to commit navigation changes");
       }
src/common/components/organisms/Navigation.tsx (2)

559-566: Click handler relies on DOM inspection which can be fragile.

Using target.closest('button[aria-label="Delete item"]') or checking tagName === 'INPUT' is fragile if the DOM structure changes. Consider using event.stopPropagation() directly on the delete button and EditableText wrapper instead.


613-630: hasUncommittedChanges() is called during render.

Calling a function that reads state during render (in the JSX condition) could cause unnecessary re-renders if the function implementation changes. Consider storing the result in a variable or using useMemo.

🔎 Proposed fix
+ // Memoize uncommitted changes check
+ const showCommitButtons = React.useMemo(
+   () => hasUncommittedChanges(),
+   [localNavigation, hasUncommittedChanges]
+ );

  {/* Commit/Cancel buttons */}
- {hasUncommittedChanges() && !shrunk && (
+ {showCommitButtons && !shrunk && (
    <div className="flex gap-2 mt-4 pt-4 border-t">
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (2)

96-107: Consider using a more descriptive spaceName instead of raw navSlug.

Currently spaceName is set to navSlug directly. For better UX, consider using the navigation item's label if available:

🔎 Proposed fix
    return {
      spaceId: localNavItem.spaceId,
-     spaceName: navSlug,
+     spaceName: localNavItem.label || navSlug,
      spaceType: SPACE_TYPES.NAV_PAGE,

110-122: 404 UI is basic but functional.

For a client component, this is an appropriate fallback. Consider adding a link back to home for better UX, or using useRouter to enable programmatic navigation.

src/pages/api/navigation/config.ts (1)

25-38: Type guard could be more thorough.

The type guard validates the shape but doesn't check if navigationConfig.items array elements have the required structure when present. Consider adding validation for item structure.

🔎 Proposed fix
function isUpdateNavigationConfigRequest(
  thing: unknown,
): thing is UpdateNavigationConfigRequest {
+ const isValidNavigationItem = (item: unknown): boolean => {
+   if (typeof item !== 'object' || item === null) return false;
+   const i = item as Record<string, unknown>;
+   return typeof i.id === 'string' && 
+          typeof i.label === 'string' && 
+          typeof i.href === 'string';
+ };
+
  return (
    isSignable(thing) &&
    typeof thing["communityId"] === "string" &&
    typeof thing["publicKey"] === "string" &&
    typeof thing["timestamp"] === "string" &&
    thing["navigationConfig"] !== undefined &&
    typeof thing["navigationConfig"] === "object" &&
    (thing["navigationConfig"]["items"] === undefined ||
-     isArray(thing["navigationConfig"]["items"]))
+     (isArray(thing["navigationConfig"]["items"]) &&
+      thing["navigationConfig"]["items"].every(isValidNavigationItem)))
  );
}
src/common/data/stores/app/navigation/navigationStore.ts (1)

252-263: TODO for space deletion left unimplemented.

Deleting navigation items with associated spaces doesn't clean up the space data. This could lead to orphaned space records in the database over time.

Would you like me to open an issue to track implementing the space deletion endpoint?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c75ffdd and 5b1e2a0.

📒 Files selected for processing (10)
  • scripts/seed.ts (2 hunks)
  • src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (4 hunks)
  • src/app/[navSlug]/[[...tabName]]/page.tsx (1 hunks)
  • src/common/components/atoms/editable-text.tsx (1 hunks)
  • src/common/components/organisms/Navigation.tsx (6 hunks)
  • src/common/components/organisms/NavigationManagement.tsx (1 hunks)
  • src/common/components/organisms/Sidebar.tsx (3 hunks)
  • src/common/data/stores/app/index.tsx (3 hunks)
  • src/common/data/stores/app/navigation/navigationStore.ts (1 hunks)
  • src/pages/api/navigation/config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/pages/api/navigation/config.ts (4)
src/config/systemConfig.ts (1)
  • NavigationConfig (155-160)
src/common/lib/signedFiles.ts (3)
  • Signable (20-23)
  • isSignable (29-41)
  • validateSignable (43-52)
src/common/data/api/requestHandler.ts (2)
  • NounspaceResponse (9-16)
  • requestHandler (31-51)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/data/stores/app/index.tsx (1)
src/common/data/stores/app/navigation/navigationStore.ts (2)
  • NavigationStore (51-51)
  • createNavigationStoreFunc (58-292)
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (2)
src/common/types/spaceData.ts (2)
  • NavPageSpacePageData (77-83)
  • SPACE_TYPES (7-13)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/common/data/stores/app/navigation/navigationStore.ts (6)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/config/systemConfig.ts (2)
  • NavigationItem (167-175)
  • NavigationConfig (155-160)
src/common/data/stores/createStore.ts (2)
  • StoreSet (51-51)
  • StoreGet (52-52)
src/common/data/stores/app/index.tsx (1)
  • AppStore (44-57)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/pages/api/space/registry/index.ts (1)
  • RegisterNewSpaceResponse (114-114)
🔇 Additional comments (9)
src/common/components/atoms/editable-text.tsx (1)

42-56: LGTM!

The event propagation handling is correctly implemented. Stopping propagation on both the input click and the display div double-click prevents unintended navigation when editing inline text within navigation items.

src/app/[navSlug]/[[...tabName]]/page.tsx (1)

30-43: LGTM with a minor observation.

The client-side fallback pattern is implemented correctly. The dynamic import of loadSystemConfig and the defensive default for adminIdentityPublicKeys are appropriate.

Consider moving the dynamic import to the top of the file as a static import if loadSystemConfig is always needed, which would improve code predictability and potentially bundle optimization. However, this is a minor point and the current approach works.

src/common/data/stores/app/index.tsx (1)

37-41: partializedNavigationStore is imported but not used in persistence.

The partializedNavigationStore is imported on line 40, but it's not included in the partialize function (lines 109-115). This means navigation state won't persist to localStorage across sessions.

If navigation changes should persist locally (for the local-first editing flow), add it to the partialize function. If navigation state is intentionally ephemeral, remove the unused import.

🔎 Proposed fix (if persistence is needed)
     partialize: (state: AppStore) => ({
       account: partializedAccountStore(state),
       homebase: partializedHomebaseStore(state),
       space: partializedSpaceStore(state),
       checkpoints: partializedCheckpointStore(state),
       chat: partializedChatStore(state),
+      navigation: partializedNavigationStore(state),
     }),

Also applies to: 109-115

src/common/components/organisms/NavigationManagement.tsx (1)

147-180: LGTM!

The Reorder.Group implementation is correct. Keys are properly set using item.id, and the drag-and-drop reordering integrates well with the debounced update callback.

src/common/components/organisms/Navigation.tsx (1)

634-655: JSX structure mismatch: <ul> tag opened but items conditionally rendered.

Lines 634-655 map navigation items inside a <ul> that starts at line 634, but the closing </ul> is at line 698, after the conditional edit button and other NavButton/NavItem components. This is correct structurally, but ensure the edit button (lines 688-697) should be inside the <ul> as it uses NavButton which renders <li>.

Also applies to: 656-698

src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (1)

69-108: Fallback logic looks correct for uncommitted navigation items.

The fallback mechanism properly checks local stores when server data is unavailable, enabling preview of uncommitted navigation items. The dependency array is comprehensive.

src/pages/api/navigation/config.ts (1)

87-96: Admin key check is correct and secure.

The authorization properly validates that the signer's public key is in the admin list before allowing updates.

src/common/data/stores/app/navigation/navigationStore.ts (2)

72-104: Good separation of concerns for navigation item creation.

The implementation correctly handles the optional space creation alongside navigation item creation, maintaining consistency between the navigation and space stores.


142-159: hasUncommittedChanges comparison is correct but could be simplified.

The logic correctly compares length, order, and key properties. Consider using isEqual from lodash for a more maintainable comparison, though the current explicit approach has the benefit of being clear about which fields matter.

Comment thread scripts/seed.ts
Comment thread src/common/components/organisms/Navigation.tsx Outdated
Comment on lines +159 to +164
const debouncedUpdateOrder = React.useCallback(
debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing cleanup for debounced function.

The debounce function creates a timer that should be cancelled on unmount to prevent memory leaks and stale updates. Additionally, the dependency array is missing from React.useCallback.

🔎 Proposed fix
- // Debounced reorder handler
- const debouncedUpdateOrder = React.useCallback(
-   debounce((newOrder: NavigationItem[]) => {
-     updateNavigationOrder(newOrder);
-   }, 300),
-   [updateNavigationOrder]
- );
+ // Debounced reorder handler
+ const debouncedUpdateOrder = React.useMemo(
+   () => debounce((newOrder: NavigationItem[]) => {
+     updateNavigationOrder(newOrder);
+   }, 300),
+   [updateNavigationOrder]
+ );
+ 
+ // Cleanup debounce on unmount
+ React.useEffect(() => {
+   return () => {
+     debouncedUpdateOrder.cancel();
+   };
+ }, [debouncedUpdateOrder]);
📝 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
const debouncedUpdateOrder = React.useCallback(
debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
// Debounced reorder handler
const debouncedUpdateOrder = React.useMemo(
() => debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
// Cleanup debounce on unmount
React.useEffect(() => {
return () => {
debouncedUpdateOrder.cancel();
};
}, [debouncedUpdateOrder]);
🤖 Prompt for AI Agents
In src/common/components/organisms/Navigation.tsx around lines 159-164, the
debouncedUpdateOrder created via debounce needs proper cleanup and stable
dependencies: move the debounced function into a ref (e.g. const debouncedRef =
useRef(debounce(...))) or create it inside useCallback but ensure debounce is
created only once, then add a useEffect that on unmount calls
debouncedRef.current.cancel() (or the debounce library's cancel/clear method) to
avoid timers leaking; also ensure the useCallback/useRef setup includes
updateNavigationOrder in its dependency handling (either rebuild the debounced
function when updateNavigationOrder changes or use a ref to the latest
updateNavigationOrder) so the dependency array is correct and linter-safe.

Comment thread src/common/data/stores/app/navigation/navigationStore.ts Outdated
Comment thread src/common/data/stores/app/navigation/navigationStore.ts Outdated
Comment thread src/pages/api/navigation/config.ts Outdated
@j-paterson
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 8, 2026

✅ Actions performed

Review triggered.

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

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

🤖 Fix all issues with AI agents
In @src/common/data/stores/app/navigation/navigationStore.ts:
- Around line 265-274: The hasUncommittedChanges check currently uses strict !==
for localItem.icon vs remoteItem.icon which will falsely flag differences when
icon is an object; update the comparison to perform a deep-equality check (e.g.,
use a utility like lodash/isEqual or compare JSON.stringify(localItem.icon) ===
JSON.stringify(remoteItem.icon)) and replace the icon inequality test inside the
state.localNavigation.some callback accordingly, keeping all other field
comparisons the same and ensuring null/undefined icons are handled consistently.
- Around line 420-434: The catch block in navigationStore.ts currently logs full
error.response.headers and serializes the entire error object which can expose
sensitive info; update the logging in that catch to omit or redact sensitive
header fields (e.g., Authorization, Cookie, Set-Cookie) and stop
JSON.stringify-ing the whole error — instead log only non-sensitive fields such
as error.response.status, error.response.statusText, and a truncated or
sanitized error.message (or a safe subset of error.response.data), or implement
a small sanitizer that removes header keys before logging; keep rethrowing the
original error unchanged.

In @tests/navigation/errorHandling.test.ts:
- Around line 9-60: Remove the local duplicate type and functions from the test
and instead export NavigationError, normalizeNavigationError, and
getUserFriendlyMessage from the production module (either add these exports to
useNavigation.ts or move them into a new errorHandling.ts and export them), then
update the test to import those exact symbols (NavigationError,
normalizeNavigationError, getUserFriendlyMessage) and use them; ensure the
exported implementations match the current logic (including axios.isAxiosError
checks and retryable logic) so tests exercise the real code rather than a local
copy.
🧹 Nitpick comments (23)
src/common/utils/tabUtils.ts (1)

36-58: Consider case-insensitive duplicate checking for consistency.

The generateUniqueName function uses exact string matching (existingNames.includes(uniqueName) on line 53), while isDuplicateTabName uses case-insensitive matching (line 32). This inconsistency could allow generateUniqueName to create names that differ only in case (e.g., "Tab" and "tab"), which isDuplicateTabName would flag as duplicates.

Consider normalizing the comparison for consistency:

♻️ Proposed fix to align case-sensitivity
 export function generateUniqueName(
   baseName: string,
   existingNames: string[],
   maxIterations: number = 100
 ): string | null {
   const trimmedBase = baseName.trim();
   let uniqueName = trimmedBase;
   let iter = 1;
+  const normalizedExisting = existingNames.map(name => name.toLowerCase());
   
-  while (existingNames.includes(uniqueName) && iter <= maxIterations) {
+  while (normalizedExisting.includes(uniqueName.toLowerCase()) && iter <= maxIterations) {
     uniqueName = `${trimmedBase} - ${iter++}`;
   }
   
   return iter > maxIterations ? null : uniqueName;
 }
src/common/components/organisms/TabBar.tsx (1)

16-16: LGTM! Consider adding user feedback for edge case.

The refactor to use generateUniqueName correctly extracts the deduplication logic into a reusable utility. The null return case is properly handled by the caller at line 169.

As an optional enhancement, consider adding user feedback if generateUniqueName returns null (when max iterations is exceeded), since the current implementation silently fails:

Optional: Add user feedback
     const uniqueName = generateUniqueTabName(sanitizedName);
     if (!uniqueName || uniqueName === tabName) {
+      if (!uniqueName) {
+        toast.error("Unable to generate a unique tab name. Please try a different name.");
+      }
       return;
     }

Also applies to: 205-205

BRANCH_REVIEW.md (1)

1-409: Consider removing this review document before merging.

This appears to be an internal code review/analysis artifact rather than permanent project documentation. Review notes documenting issues and recommendations are valuable during PR review but typically should not be committed to the repository.

If this analysis should persist, consider:

  1. Moving actionable items to GitHub issues
  2. Converting stable recommendations to ADRs (Architecture Decision Records) or contributing guidelines
  3. Adding a docs/reviews/ directory if review artifacts are intentionally retained
REFACTOR_REVIEW.md (1)

1-287: Same concern: review artifact should not be committed.

Like BRANCH_REVIEW.md, this is a review document that would be better suited as PR comments, issues, or ADRs rather than committed markdown files. Consider removing before merge or relocating to appropriate documentation structure.

src/common/data/stores/app/space/spaceStore.ts (1)

691-740: Robust error handling for space tab loading.

Good defensive programming with:

  • HTML 404 detection in both the catch block and after successful fetch (covers different axios configurations)
  • Separate try-catch blocks for JSON parsing and decryption with specific error messages
  • Graceful fallback to local data when remote tab doesn't exist

The duplicate HTML detection logic (lines 704-711 and 718-721) could be extracted to a helper function for DRY, but the current approach is acceptable.

Optional: Extract HTML detection helper
// Helper at module level
const isHtml404Response = (text: string): boolean => {
  const trimmed = text.trim();
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html");
};
src/common/utils/navUtils.ts (1)

49-58: Misleading comment about protocol-relative URLs.

The comment on line 55 mentions "protocol-relative URLs" but:

  1. Protocol-relative URLs (//example.com) start with // and wouldn't pass the path regex on line 52
  2. The actual intent appears to be preventing malformed paths like /foo//bar

Consider updating the comment for clarity:

Suggested comment fix
-    // Prevent double slashes (except at start for protocol-relative URLs)
+    // Prevent double slashes in path segments (e.g., "/foo//bar")
     if (trimmedHref.includes("//") && !trimmedHref.startsWith("//")) {
src/common/components/atoms/editable-text.tsx (2)

35-52: Validation flow works but UX could be improved.

The validation logic is correct - it resets text and stays in edit mode on error. However:

  1. User feedback: console.warn (line 46) won't be visible to users. Consider surfacing the validation error in the UI (tooltip, toast, or inline message).

  2. Edit mode behavior: On validation failure, the text resets to initialText but editing continues. This might confuse users who don't understand why their input was rejected.

Consider showing the validation error to users:

Optional: Add user-facing feedback
+import { toast } from "sonner"; // or your toast library

 if (validationError) {
   // Reset text to original and stay in edit mode
   settext(initialText);
-  console.warn("Invalid input:", validationError);
+  toast.error(validationError);
   return; // Don't exit edit mode, don't call updateMethod
 }

75-85: Double-click works but lacks keyboard accessibility.

The double-click to edit works correctly with proper event handling. However, there's no keyboard-accessible way to enter edit mode. Consider adding:

  • tabIndex={0} to make the element focusable
  • onKeyDown handler to enter edit mode on Enter/Space
Optional: Add keyboard accessibility
 <div 
-  className="select-none min-w-0" 
+  className="select-none min-w-0 cursor-pointer" 
+  tabIndex={0}
+  role="button"
+  aria-label={`Edit ${text}`}
   onDoubleClick={(e) => {
     e.preventDefault();
     e.stopPropagation();
     setisEditing(true);
   }}
+  onKeyDown={(e) => {
+    if (e.key === "Enter" || e.key === " ") {
+      e.preventDefault();
+      setisEditing(true);
+    }
+  }}
 >
src/common/components/organisms/navigation/ErrorBoundary.tsx (1)

61-67: Consider accepting a reset callback prop for more robust recovery.

The handleReset method clears error state, but if the underlying error is deterministic (e.g., bad navigation data), re-rendering children will immediately re-throw. Consider adding an optional onReset prop that allows parent components to perform additional cleanup (e.g., resetting navigation state to a known good value) before re-rendering.

💡 Suggested enhancement
 interface ErrorBoundaryProps {
   children: ReactNode;
   fallback?: ReactNode;
   onError?: (error: Error, errorInfo: ErrorInfo) => void;
+  onReset?: () => void;
 }

 // ...

   handleReset = () => {
+    this.props.onReset?.();
     this.setState({
       hasError: false,
       error: null,
       errorInfo: null,
     });
   };
tests/navigation/errorHandling.test.ts (1)

100-111: Consider using Vitest's mocking utilities for cleaner axios mocking.

The manual override of axios.isAxiosError works but is fragile. Vitest provides cleaner mocking utilities.

💡 Cleaner approach with vi.spyOn
import { vi } from 'vitest';

it('should normalize network errors from axios', () => {
  const axiosError = Object.assign(new Error('Request failed'), {
    isAxiosError: true,
    response: {
      status: 500,
      data: { error: { message: 'Internal server error' } },
    },
  });
  
  vi.spyOn(axios, 'isAxiosError').mockImplementation((err) => err === axiosError);
  
  const normalized = normalizeNavigationError(axiosError);
  
  expect(normalized.type).toBe('NETWORK');
  expect(normalized.retryable).toBe(true);
  
  vi.restoreAllMocks();
});
src/common/components/organisms/navigation/useNavigation.ts (1)

119-125: Unused destructured values: deleteNavigationItem and renameNavigationItem.

These are destructured from the store but never used within this hook. If they're intended for external use, they should be included in the return value. Otherwise, remove them to avoid confusion.

♻️ Remove unused destructuring
   const {
     loadNavigation,
     localNavigation,
     hasUncommittedChanges,
     createNavigationItem,
-    deleteNavigationItem,
-    renameNavigationItem,
     updateNavigationOrder,
     commitNavigationChanges,
     resetNavigationChanges,
   } = useAppStore((state) => ({
     loadNavigation: state.navigation.loadNavigation,
     localNavigation: state.navigation.localNavigation,
     hasUncommittedChanges: state.navigation.hasUncommittedChanges,
     createNavigationItem: state.navigation.createNavigationItem,
-    deleteNavigationItem: state.navigation.deleteNavigationItem,
-    renameNavigationItem: state.navigation.renameNavigationItem,
     updateNavigationOrder: state.navigation.updateNavigationOrder,
     commitNavigationChanges: state.navigation.commitNavigationChanges,
     resetNavigationChanges: state.navigation.resetNavigationChanges,
   }));
src/common/components/organisms/navigation/NavigationEditor.tsx (2)

207-212: Remove empty onKeyDown handler.

This handler checks a condition but does nothing - navigation is already handled by the Link component. Empty handlers can be confusing and suggest incomplete implementation.

♻️ Remove the empty handler
-                  onKeyDown={(e) => {
-                    // Allow keyboard navigation but prevent during drag
-                    if (e.key === "Enter" && !e.currentTarget.closest('[data-dragging="true"]')) {
-                      // Navigation handled by Link component
-                    }
-                  }}

233-254: Extract duplicate delete logic into a single handler.

The click and keydown handlers duplicate the same onDelete + toast logic.

♻️ Deduplicate delete handlers
+                    const handleDeleteItem = (e: React.SyntheticEvent) => {
+                      e.preventDefault();
+                      e.stopPropagation();
+                      onDelete(item.id);
+                      toast.success("Navigation item deleted");
+                    };
+
                     <button
-                      onClick={(e) => {
-                        e.preventDefault();
-                        e.stopPropagation();
-                        onDelete(item.id);
-                        toast.success("Navigation item deleted");
-                      }}
+                      onClick={handleDeleteItem}
                       onKeyDown={(e) => {
                         if (e.key === "Enter" || e.key === " ") {
-                          e.preventDefault();
-                          e.stopPropagation();
-                          onDelete(item.id);
-                          toast.success("Navigation item deleted");
+                          handleDeleteItem(e);
                         }
                       }}
src/common/components/organisms/Navigation.tsx (3)

266-270: Consider combining store selectors to reduce subscriptions.

This creates a second useAppStore subscription in addition to the one in useNavigation hook (which also destructures deleteNavigationItem and renameNavigationItem but doesn't use them). Consider either:

  1. Having useNavigation return these handlers, or
  2. Combining this with the existing useAppStore call at lines 70-76

Multiple subscriptions can cause unnecessary re-renders.

♻️ Option: Return handlers from useNavigation

In useNavigation.ts, include these in the return:

return {
  // ... existing returns
  deleteNavigationItem,
  renameNavigationItem,
};

Then in Navigation.tsx:

   const {
     localNavigation,
     hasUncommittedChanges,
     navItemsToDisplay,
     debouncedUpdateOrder,
     handleCommit,
     handleCancel,
     handleCreateItem,
+    deleteNavigationItem,
+    renameNavigationItem,
   } = useNavigation(systemConfig, setNavEditMode);
   
-  // Get store functions for editor
-  const { deleteNavigationItem, renameNavigationItem } = useAppStore((state) => ({
-    deleteNavigationItem: state.navigation.deleteNavigationItem,
-    renameNavigationItem: state.navigation.renameNavigationItem,
-  }));

370-370: Simplify redundant conditional.

username is already string | undefined (line 227), so username ? username : undefined is equivalent to just username.

-                  username={username ? username : undefined}
+                  username={username}

467-481: Extract inline icon component to avoid recreation on each render.

The inline arrow function at line 470-474 creates a new component on every render, which can cause unnecessary re-renders of NavigationButton.

♻️ Extract to a stable component reference
+const EditNavIcon = () => (
+  <div className="w-6 h-6 flex items-center justify-center">
+    <FaPencil className="w-4 h-4 text-gray-800 dark:text-white" />
+  </div>
+);

 // In the component:
                 {isNavigationEditable && !mobile && (
                   <NavigationButton
                     label={navEditMode ? "Exit Edit" : "Edit Nav"}
-                    Icon={navEditMode ? LogoutIcon : () => (
-                      <div className="w-6 h-6 flex items-center justify-center">
-                        <FaPencil className="w-4 h-4 text-gray-800 dark:text-white" />
-                      </div>
-                    )}
+                    Icon={navEditMode ? LogoutIcon : EditNavIcon}
                     onClick={() => {
                       setNavEditMode(!navEditMode);
                     }}
src/common/components/organisms/navigation/NavigationItem.tsx (1)

84-102: Redundant e.preventDefault() call on line 96.

The e.preventDefault() on line 93 already prevents the default behavior for both Enter and Space keys. The nested call on line 96 is unnecessary.

Suggested simplification
  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLAnchorElement>) => {
      if (disable) {
        e.preventDefault();
        return;
      }

      // Enter or Space key activates the link
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
-       if (e.key === " ") {
-         // Prevent page scroll on Space
-         e.preventDefault();
-       }
        handleClick(e as unknown as React.MouseEvent<HTMLAnchorElement>);
      }
    },
    [disable, handleClick]
  );
src/pages/api/navigation/config.ts (2)

80-81: Remove debug logging before production.

Line 81 logs navigation items including IDs, labels, and hrefs. This should be removed or guarded behind a debug flag to avoid log pollution in production.

Suggested fix
-  // Debug: log what we received
-  console.log("Received navigation items:", updateRequest.navigationConfig.items?.map((item: NavigationItem) => ({ id: item.id, label: item.label, href: item.href })));
+  // Debug logging - enable only in development
+  if (process.env.NODE_ENV === 'development') {
+    console.log("Received navigation items:", updateRequest.navigationConfig.items?.map((item: NavigationItem) => ({ id: item.id, label: item.label, href: item.href })));
+  }

103-112: Potential information disclosure in error message.

The 403 error response includes the public key and community ID. While the public key is not secret, including it in error messages could aid reconnaissance. Consider a more generic message.

Suggested fix
  if (!isArray(adminKeys) || !adminKeys.includes(updateRequest.publicKey)) {
    res.status(403).json({
      result: "error",
      error: {
-       message: `Identity ${updateRequest.publicKey} is not an admin for community ${updateRequest.communityId}`,
+       message: "You do not have permission to update navigation for this community.",
      },
    });
    return;
  }
CRITIQUE_VERIFICATION.md (1)

1-214: Consider whether this verification document should be committed.

This document is useful for tracking that review feedback was addressed, but it may be more appropriate as a PR comment or wiki page rather than a committed file. If kept, consider:

  1. Moving to a /docs or /notes directory
  2. Adding a note that it's internal documentation
  3. Removing it after merge if it's only for review purposes

The document does provide valuable context about the error handling architecture, so keeping a refined version could aid future maintainers.

src/common/data/stores/app/navigation/navigationStore.ts (3)

117-122: Fallback href uses Date.now() which could collide in rapid succession.

If the generated href validation fails (noted as "shouldn't happen"), the fallback /item-${Date.now()} could theoretically collide if called multiple times within the same millisecond. Consider using the already-generated newId (UUID) for guaranteed uniqueness.

Suggested fix
+    const newId = uuidv4();
+    
     // Auto-generate href from label, ensuring uniqueness
     trimmedHref = generateUniqueHrefFromLabel(uniqueLabel, state.localNavigation);
     // Validate generated href (should always be valid, but check anyway)
     const hrefError = validateNavItemHref(trimmedHref);
     if (hrefError) {
       // This shouldn't happen with our generator, but handle it gracefully
-      trimmedHref = `/item-${Date.now()}`;
+      trimmedHref = `/item-${newId}`;
     }
   }
   
-  const newId = uuidv4();
   const spaceId = uuidv4();

137-163: Two separate set calls create non-atomic state updates.

The space creation (lines 137-159) and navigation item addition (lines 161-163) are in separate set calls. If an error occurs between them, state could be inconsistent (space exists without nav item). Consider combining into a single set call.

Suggested fix
-    set((draft) => {
-      // Add to space store's localSpaces so PublicSpace can find it
-      // Initialize with default "Home" tab so the space can be viewed immediately
-      draft.space.localSpaces[spaceId] = {
-        id: spaceId,
-        updatedAt: timestamp,
-        tabs: {
-          Home: {
-            ...INITIAL_SPACE_CONFIG_EMPTY,
-            theme: {
-              ...INITIAL_SPACE_CONFIG_EMPTY.theme,
-              id: `${spaceId}-Home-theme`,
-              name: `${spaceId}-Home-theme`,
-            },
-            isPrivate: false,
-            timestamp: timestamp,
-          },
-        },
-        order: ["Home"], // Initial default tab
-        changedNames: {},
-        deletedTabs: [],
-      };
-    }, "createNavigationItem-addLocalSpace");
-    
-    set((draft) => {
-      draft.navigation.localNavigation.push(cloneDeep(newItem));
-    }, "createNavigationItem");
+    set((draft) => {
+      // Add to space store's localSpaces so PublicSpace can find it
+      // Initialize with default "Home" tab so the space can be viewed immediately
+      draft.space.localSpaces[spaceId] = {
+        id: spaceId,
+        updatedAt: timestamp,
+        tabs: {
+          Home: {
+            ...INITIAL_SPACE_CONFIG_EMPTY,
+            theme: {
+              ...INITIAL_SPACE_CONFIG_EMPTY.theme,
+              id: `${spaceId}-Home-theme`,
+              name: `${spaceId}-Home-theme`,
+            },
+            isPrivate: false,
+            timestamp: timestamp,
+          },
+        },
+        order: ["Home"],
+        changedNames: {},
+        deletedTabs: [],
+      };
+      // Add navigation item in the same transaction
+      draft.navigation.localNavigation.push(cloneDeep(newItem));
+    }, "createNavigationItem");

391-397: Remove debug logging or guard with environment check.

Similar to the API endpoint, this debug logging should be removed or guarded for production.

Suggested fix
-      // Debug: log what we're about to send
-      console.log("Committing navigation config:", {
-        items: itemsToCommit.map(item => ({ id: item.id, label: item.label, href: item.href })),
-        logoTooltip: navigationConfigToCommit.logoTooltip,
-        showMusicPlayer: navigationConfigToCommit.showMusicPlayer,
-        showSocials: navigationConfigToCommit.showSocials,
-      });
+      // Debug logging - enable only in development
+      if (process.env.NODE_ENV === 'development') {
+        console.log("Committing navigation config:", {
+          items: itemsToCommit.map(item => ({ id: item.id, label: item.label, href: item.href })),
+          logoTooltip: navigationConfigToCommit.logoTooltip,
+          showMusicPlayer: navigationConfigToCommit.showMusicPlayer,
+          showSocials: navigationConfigToCommit.showSocials,
+        });
+      }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1e2a0 and 4b775ca.

📒 Files selected for processing (26)
  • BRANCH_REVIEW.md
  • CRITIQUE_VERIFICATION.md
  • NAVIGATION_REFACTOR_PLAN.md
  • REFACTOR_REVIEW.md
  • docs/GETTING_STARTED.md
  • docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md
  • src/common/components/atoms/editable-text.tsx
  • src/common/components/atoms/reorderable-tab.tsx
  • src/common/components/organisms/Navigation.tsx
  • src/common/components/organisms/NavigationManagement.tsx
  • src/common/components/organisms/Sidebar.tsx
  • src/common/components/organisms/TabBar.tsx
  • src/common/components/organisms/navigation/ErrorBoundary.tsx
  • src/common/components/organisms/navigation/NavigationEditor.tsx
  • src/common/components/organisms/navigation/NavigationItem.tsx
  • src/common/components/organisms/navigation/constants.ts
  • src/common/components/organisms/navigation/useNavigation.ts
  • src/common/data/stores/app/index.tsx
  • src/common/data/stores/app/navigation/navigationStore.ts
  • src/common/data/stores/app/space/spaceStore.ts
  • src/common/utils/layoutFormatUtils.ts
  • src/common/utils/navUtils.ts
  • src/common/utils/tabUtils.ts
  • src/config/loaders/runtimeLoader.ts
  • src/pages/api/navigation/config.ts
  • tests/navigation/errorHandling.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/common/components/organisms/NavigationManagement.tsx
  • src/common/components/organisms/Sidebar.tsx
  • src/common/data/stores/app/index.tsx
🧰 Additional context used
🧬 Code graph analysis (11)
src/common/components/organisms/navigation/ErrorBoundary.tsx (1)
src/common/components/atoms/button.tsx (1)
  • Button (81-81)
src/common/components/organisms/TabBar.tsx (1)
src/common/utils/tabUtils.ts (1)
  • generateUniqueName (44-58)
src/common/data/stores/app/space/spaceStore.ts (4)
src/common/data/database/supabase/clients/api.ts (2)
  • set (14-16)
  • get (11-13)
src/common/lib/signedFiles.ts (1)
  • SignedFile (16-18)
src/pages/api/space/registry/[spaceId]/index.ts (1)
  • UpdateTabOrderRequest (30-30)
src/pages/api/space/homebase/tabOrder.ts (1)
  • UpdateTabOrderRequest (15-15)
src/common/components/organisms/navigation/useNavigation.ts (2)
src/common/components/organisms/navigation/NavigationItem.tsx (1)
  • NavigationItem (139-153)
src/common/components/organisms/navigation/constants.ts (1)
  • NAVIGATION_REORDER_DEBOUNCE_MS (14-14)
src/pages/api/navigation/config.ts (7)
src/config/systemConfig.ts (1)
  • NavigationConfig (155-160)
src/common/lib/signedFiles.ts (3)
  • Signable (20-23)
  • isSignable (29-41)
  • validateSignable (43-52)
src/common/data/api/requestHandler.ts (2)
  • NounspaceResponse (9-16)
  • requestHandler (31-51)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/utils/navUtils.ts (2)
  • validateNavItemLabel (19-35)
  • validateNavItemHref (42-72)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/supabase/database.d.ts (1)
  • Json (3-9)
src/common/utils/navUtils.ts (3)
src/common/components/organisms/navigation/constants.ts (2)
  • NAVIGATION_MAX_LABEL_LENGTH (23-23)
  • NAVIGATION_MAX_UNIQUE_NAME_ITERATIONS (32-32)
src/common/components/organisms/navigation/NavigationItem.tsx (1)
  • NavigationItem (139-153)
src/common/utils/tabUtils.ts (2)
  • isDuplicateTabName (25-34)
  • generateUniqueName (44-58)
src/common/components/organisms/navigation/NavigationItem.tsx (4)
src/common/lib/hooks/useUIColors.ts (1)
  • useUIColors (14-28)
src/common/components/atoms/badge.tsx (1)
  • Badge (40-40)
src/fidgets/ui/Links.tsx (1)
  • Link (27-32)
src/common/lib/utils/mergeClasses.ts (1)
  • mergeClasses (4-6)
src/common/components/atoms/reorderable-tab.tsx (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)
  • renameTab (375-497)
src/common/components/organisms/Navigation.tsx (5)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (55-57)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/common/components/organisms/navigation/useNavigation.ts (1)
  • useNavigation (101-267)
src/common/components/organisms/navigation/NavigationEditor.tsx (1)
  • NavigationEditor (365-365)
src/common/components/organisms/navigation/NavigationItem.tsx (1)
  • NavigationButton (224-236)
src/common/components/atoms/editable-text.tsx (1)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/common/utils/layoutFormatUtils.ts (1)
src/common/fidgets/index.d.ts (3)
  • LayoutFidgetDetails (114-122)
  • LayoutFidgetConfig (110-112)
  • FidgetInstanceData (94-101)
🪛 LanguageTool
NAVIGATION_REFACTOR_PLAN.md

[style] ~6-~6: To elevate your writing, try using a synonym here.
Context: ... Issues: Too large, mixed concerns, hard to test/maintain ## Recommended Split ...

(HARD_TO)

🔇 Additional comments (33)
src/config/loaders/runtimeLoader.ts (1)

17-17: Verify environment variable is set correctly in all environments.

The environment variable has been renamed from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY. Ensure this variable is properly configured in all deployment environments and that the fallback to NEXT_PUBLIC_SUPABASE_ANON_KEY is intentional for your use case.

docs/GETTING_STARTED.md (1)

72-72: LGTM!

The environment variable name has been correctly updated in the example configuration, aligning with the changes in the architecture documentation and runtime loader.

src/common/components/organisms/navigation/constants.ts (1)

1-32: LGTM! Well-documented constants file.

Good practice to centralize magic numbers with clear documentation. The values are reasonable: 300ms debounce balances responsiveness with API efficiency, 50-char label limit is appropriate for navigation UI, and 100 iterations provides a safe upper bound.

src/common/data/stores/app/space/spaceStore.ts (3)

260-291: Good defensive initialization for new spaces.

The initialization of remoteSpaces[spaceId] before updating tabs handles the case where a new space hasn't been synced yet. The guard on line 281 correctly prevents deleting non-existent entries.

One minor observation: the try block starting at line 260 has inconsistent indentation - it appears to be missing a leading space compared to surrounding code.


607-617: Initialization ensures safe batch operations.

Pre-initializing remoteSpaces[spaceId] before the batch commit prevents undefined access errors. While this could theoretically be combined with the final sync set() call at line 651 for atomicity, the current approach is clearer and the initialization is idempotent.


827-838: Consistent 404 handling for tab order.

Good addition that gracefully handles spaces without a committed tab order file, allowing local order to be used as fallback. This complements the similar handling in loadSpaceTab.

src/common/utils/navUtils.ts (5)

19-35: Label validation looks good with appropriate character allowlist.

The regex [^a-zA-Z0-9-_ $] correctly allows alphanumeric, hyphens, underscores, spaces, and $ (for token symbols). Length validation uses the centralized constant.


82-90: Good code reuse with tab utilities.

Clean delegation to isDuplicateTabName with appropriate data transformation.


99-108: Case-sensitive href comparison is correct.

URLs and paths should be compared case-sensitively, unlike labels. Implementation is straightforward and correct.


118-154: Well-implemented slug generation with appropriate fallbacks.

The slugification logic correctly:

  • Lowercases and replaces non-alphanumeric characters with hyphens
  • Trims leading/trailing hyphens
  • Falls back to "item" for empty slugs
  • Uses timestamp fallback if max iterations exceeded (edge case safety)

164-179: Consistent code reuse with appropriate fallback.

Good delegation to generateUniqueName with a timestamp-based fallback matching the pattern in generateUniqueHrefFromLabel.

src/common/utils/layoutFormatUtils.ts (4)

7-11: Good undefined-safe implementation.

The function correctly handles undefined input by returning false, maintaining backward compatibility while allowing callers to pass potentially undefined layout details.


19-33: Clean early return with sensible default.

Returning 'grid' for undefined layout details is a reasonable fallback that maintains consistent behavior.


41-56: Appropriate fallback for undefined layouts.

Returning all fidget keys when layout is undefined ensures fidgets are still displayed, just without a specific mobile ordering.


103-118: Consistent defensive handling with safe defaults.

Both the undefined check (line 107-109) and the fallback for missing layoutConfig (line 117) ensure callers always receive a valid object structure.

src/common/components/atoms/editable-text.tsx (3)

12-24: Clean prop interface with optional validation.

Good design allowing callers to provide custom validation while defaulting to tab name validation. The maxLength default of 22 should be extracted to a constant if it has semantic meaning.


28-33: Correct synchronization pattern.

The effect properly syncs with initialText changes only when not editing, preventing loss of user input during edits. Dependencies are correctly specified.


54-73: Good event handling on input.

The stopPropagation on click (lines 68-71) correctly prevents click events from bubbling to parent navigation elements, avoiding accidental navigation while editing.

src/common/components/atoms/reorderable-tab.tsx (1)

85-91: LGTM!

Good defensive check. This ensures EditableText only renders when renameTab is actually provided, preventing potential undefined callback issues and aligning edit mode behavior with callback availability.

src/common/components/organisms/navigation/ErrorBoundary.tsx (1)

22-112: Well-structured error boundary implementation.

The error boundary correctly follows React patterns with proper state management, development-only error details, and a clean fallback UI. The TODO for error logging service integration is a good reminder for production readiness.

NAVIGATION_REFACTOR_PLAN.md (1)

1-307: Comprehensive refactoring plan.

This is a well-structured design document that clearly outlines the decomposition strategy, migration phases, and expected benefits. The phased approach (hooks → sub-components → main split) with "quick wins first" prioritization is pragmatic.

One minor note: line number references (e.g., "Extract from: Lines 544-733") will become stale as the codebase evolves. Consider removing or updating these references as the refactor progresses.

src/common/components/organisms/navigation/useNavigation.ts (3)

137-143: LGTM - Well-designed initial load guard.

The ref-based pattern correctly prevents re-loading after commits while still loading on initial mount when the store is empty. The comment clearly explains the intent.


152-164: LGTM - Proper debounce with cleanup.

The debounced reorder handler is correctly memoized and cleaned up on unmount, preventing memory leaks and stale updates.


258-266: LGTM - Clean return interface.

The return value properly exposes navigation state and handlers. The hasUncommittedChanges is correctly evaluated at render time (since it's a store selector function).

src/common/components/organisms/navigation/NavigationEditor.tsx (1)

302-358: LGTM - System items display.

Showing system navigation items (Notifications, Search, My Space, Login/Logout) as disabled during edit mode provides good visual context while preventing accidental interaction. The conditional rendering based on isLoggedIn and isInitializing is correct.

src/common/components/organisms/Navigation.tsx (1)

355-388: LGTM - Good error boundary integration.

The NavigationErrorBoundary correctly wraps NavigationEditor to isolate editing-related errors. The onError callback with production logging placeholder is a good practice.

src/common/components/organisms/navigation/NavigationItem.tsx (3)

55-79: LGTM! Well-structured client-side navigation with proper modifier key handling.

The click handler correctly distinguishes between internal navigation (using router.push with startTransition) and external links. The modifier key detection (metaKey, ctrlKey, etc.) properly allows users to open links in new tabs.


139-153: Memo comparator omits Icon and function props, which may cause stale closures.

The custom comparator ignores Icon, onClick, and onNavigate. If these props change (e.g., onClick captures different closure variables), the component won't re-render. While Icon is typically stable, callback changes could lead to stale behavior.

Consider whether function props need comparison or if they're guaranteed stable via useCallback in parent components.


224-236: NavigationButton memo comparator also omits onClick, similar concern.

Same potential issue as NavigationItem—if onClick changes identity and captures different state, the memoized component won't update.

src/pages/api/navigation/config.ts (3)

176-224: Space deletion errors are logged but don't fail the request—verify this is intentional.

When deleting spaces for removed navigation items, storage and registration deletion errors are caught and logged with console.warn but the request continues. This is a deliberate design choice (as noted in the comment), but could leave orphaned data if deletions repeatedly fail.

Consider adding monitoring/alerting for these warnings or implementing a cleanup job for orphaned spaces.


233-235: JSON round-trip for type conversion is correct but adds overhead.

The JSON.parse(JSON.stringify(...)) pattern ensures JSON compatibility but incurs serialization cost. For navigation configs (typically small), this is acceptable.


266-279: Good defensive check for race conditions.

Verifying that rows were actually updated catches edge cases where the config was deleted or unpublished between fetch and update. The 409 status code is appropriate for this conflict scenario.

src/common/data/stores/app/navigation/navigationStore.ts (1)

75-82: LGTM! Clean initialization with proper filtering and deep cloning.

The loadNavigation function correctly filters out hardcoded items and uses cloneDeep to prevent mutation of the source data.

Comment thread src/common/components/organisms/navigation/NavigationEditor.tsx Outdated
Comment on lines +265 to +274
return state.localNavigation.some((localItem, index) => {
const remoteItem = state.remoteNavigation[index];
return !remoteItem ||
localItem.id !== remoteItem.id ||
localItem.label !== remoteItem.label ||
localItem.href !== remoteItem.href ||
localItem.icon !== remoteItem.icon ||
localItem.spaceId !== remoteItem.spaceId;
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

hasUncommittedChanges comparison may fail for object-type icon property.

Line 271 uses strict equality (!==) for icon comparison. If icon is an object, reference comparison will fail even when values are equal. Consider deep comparison or JSON stringification for the icon field.

Suggested fix
     return state.localNavigation.some((localItem, index) => {
       const remoteItem = state.remoteNavigation[index];
       return !remoteItem || 
         localItem.id !== remoteItem.id ||
         localItem.label !== remoteItem.label ||
         localItem.href !== remoteItem.href ||
-        localItem.icon !== remoteItem.icon ||
+        JSON.stringify(localItem.icon) !== JSON.stringify(remoteItem.icon) ||
         localItem.spaceId !== remoteItem.spaceId;
     });
🤖 Prompt for AI Agents
In @src/common/data/stores/app/navigation/navigationStore.ts around lines 265 -
274, The hasUncommittedChanges check currently uses strict !== for
localItem.icon vs remoteItem.icon which will falsely flag differences when icon
is an object; update the comparison to perform a deep-equality check (e.g., use
a utility like lodash/isEqual or compare JSON.stringify(localItem.icon) ===
JSON.stringify(remoteItem.icon)) and replace the icon inequality test inside the
state.localNavigation.some callback accordingly, keeping all other field
comparisons the same and ensuring null/undefined icons are handled consistently.

Comment thread src/common/data/stores/app/navigation/navigationStore.ts Outdated
Comment on lines +420 to +434
} catch (error: any) {
console.error("Failed to commit navigation changes:", error);
// Log full error response for debugging
if (error?.response) {
console.error("API error response:", {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
headers: error.response.headers,
});
} else if (error?.message) {
console.error("Error message:", error.message);
}
console.error("Full error object:", JSON.stringify(error, null, 2));
throw error;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error logging exposes potentially sensitive data including headers.

Line 428 logs response headers which could include authentication tokens or other sensitive information. Consider limiting logged fields.

Suggested fix
     } catch (error: any) {
       console.error("Failed to commit navigation changes:", error);
       // Log full error response for debugging
       if (error?.response) {
         console.error("API error response:", {
           status: error.response.status,
           statusText: error.response.statusText,
           data: error.response.data,
-          headers: error.response.headers,
         });
       } else if (error?.message) {
         console.error("Error message:", error.message);
       }
-      console.error("Full error object:", JSON.stringify(error, null, 2));
       throw error;
     }
🤖 Prompt for AI Agents
In @src/common/data/stores/app/navigation/navigationStore.ts around lines 420 -
434, The catch block in navigationStore.ts currently logs full
error.response.headers and serializes the entire error object which can expose
sensitive info; update the logging in that catch to omit or redact sensitive
header fields (e.g., Authorization, Cookie, Set-Cookie) and stop
JSON.stringify-ing the whole error — instead log only non-sensitive fields such
as error.response.status, error.response.statusText, and a truncated or
sanitized error.message (or a safe subset of error.response.data), or implement
a small sanitizer that removes header keys before logging; keep rethrowing the
original error unchanged.

Comment thread tests/navigation/errorHandling.test.ts Outdated
@j-paterson
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

✅ Actions performed

Review triggered.

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

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

🤖 Fix all issues with AI agents
In @docs/README.md:
- Around line 36-42: "Customization" is under the wrong heading; move the
"Customization" list item so it is nested with the "Themes" section. Locate the
"Themes" heading and its list (the "Theme System" bullet) and reattach the
"Customization" bullet (currently listed under the "Navigation" heading)
directly beneath the "Themes" section; ensure the "Navigation" section only
contains its intended bullets (e.g., "Navigation System") and remove the
orphaned "Customization" entry from "Navigation".

In @docs/SYSTEMS/NAVIGATION/OVERVIEW.md:
- Around line 148-154: Update the documentation to match the code: in the
NavigationConfig declaration (logoTooltip) mark href as optional (e.g.,
logoTooltip?: { text: string; href?: string }) or otherwise note that href is
optional to match src/config/systemConfig.ts; ensure the docs refer to
logoTooltip (or LogoTooltipConfig if used) and show href?: string instead of
href: string.

In @src/common/components/organisms/Navigation.tsx:
- Around line 298-333: iconFor currently creates new functional components
(ReactIconWrapper/CustomIconWrapper) on each call causing remounts; change
iconFor to return stable component references by caching wrapper components per
key: create a useRef<Map<string, React.FC>> or a useMemo-generated lookup object
keyed by icon key that stores and reuses wrapper components for iconPack entries
and URL icons, and for URL icons use a single reusable ImgIcon component factory
that reads src via props or returns a memoized wrapper for that src; update
iconFor to check the cache first and only create/store a new wrapper if one
doesn't exist (still fall back to HomeIcon), keeping the existing symbol names
iconFor, iconPack, ReactIconWrapper/CustomIconWrapper and HomeIcon for clarity.

In @src/common/components/organisms/navigation/NavigationItem.tsx:
- Around line 92-102: Remove the duplicate e.preventDefault() and stop casting
the keyboard event to a MouseEvent; instead make handleClick accept a union
event type (e.g., React.MouseEvent<HTMLAnchorElement> |
React.KeyboardEvent<HTMLAnchorElement>) and update its internals to guard any
mouse-only properties (use type guards or checks like 'button' in event and
optional chaining for metaKey/ctrlKey) before accessing them, then call
handleClick(e) directly from the onKeyDown handler when Enter or Space is
pressed.

In @src/common/data/stores/app/navigation/navigationStore.ts:
- Line 596: The current console.error call uses JSON.stringify(error, null, 2)
which can throw on circular structures (common with Axios errors); replace it
with a safe serialization approach: either use Node's util.inspect(error, {
depth: null }) or wrap JSON.stringify in try/catch and fall back to
error.toString() or logging specific fields (error.message, error.stack,
error.response?.data) so the logging line that references error (the
console.error("Full error object:", JSON.stringify(error, null, 2)) statement)
never throws and always outputs useful error details.
- Around line 195-209: deleteNavigationItem currently removes the entry from
navigation.localNavigation but doesn't remove its associated local space,
causing buildup; modify deleteNavigationItem so it first finds the item (using
get().navigation.localNavigation.find) to obtain item.spaceId, then inside the
set(draft => { ... }, "deleteNavigationItem") block delete the corresponding
entry from draft.space.localSpaces (e.g., if (item?.spaceId) delete
draft.space.localSpaces[item.spaceId]) before or after filtering
draft.navigation.localNavigation; ensure you guard for missing item/spaceId to
avoid exceptions and keep the existing debug logs.
- Around line 337-345: The icon field is being compared by reference in
hasUncommittedChanges (state.localNavigation / localItem.icon vs
state.remoteNavigation / remoteItem.icon), which yields false positives for
equivalent icon objects; replace the reference check localItem.icon !==
remoteItem.icon with a deep equality check (e.g., use a stable deep-equal
utility like lodash/isEqual or a project deepEqual helper) and guard for
null/undefined so two absent icons are considered equal, keeping the rest of the
property comparisons unchanged.
- Around line 156-178: The two separate set calls in createNavigationItem risk
leaving draft.space.localSpaces[spaceId] orphaned if the subsequent set adding
the navigation item fails; combine the updates into a single set call so both
the new local space and its corresponding navigation item are added atomically:
inside the same set(draft => { ... }, "...") update both
draft.space.localSpaces[spaceId] (with id, updatedAt, tabs, order, changedNames,
deletedTabs) and the draft.navigation/whatever structure where the navigation
item is created (the code that was in the second set call), ensuring both
mutations occur together to avoid inconsistent state.
- Around line 565-568: The axiosBackend.put call in navigationStore that sends
signedRequest should validate the returned response body before treating the
update as successful: capture the result of await
axiosBackend.put("/api/navigation/config", signedRequest), check response.data
for an error (e.g., response.data?.error?.message) or an explicit success flag,
and if an error is present throw or handle it the same way as the pattern used
at the other call (line ~484) so the store does not update state on 2xx error
payloads; update the console log block around navigationStore to use the
validated response and only proceed when the payload indicates success.

In @src/pages/api/navigation/config.ts:
- Around line 31-44: The type guard isUpdateNavigationConfigRequest currently
permits navigationConfig.items to be undefined while the NavigationConfig
interface demands items: NavigationItem[]; either tighten the guard to require
navigationConfig.items be an array (remove the allowance for undefined in the
check of thing["navigationConfig"]["items"]) so only payloads with items pass,
or if undefined is acceptable make NavigationConfig.items optional (items?:
NavigationItem[]) and update the UpdateNavigationConfigRequest type accordingly;
locate and change the check inside isUpdateNavigationConfigRequest or update the
NavigationConfig/UpdateNavigationConfigRequest type definitions to keep them
consistent.
- Around line 276-284: The multi-step operation that calls storage deletions and
space-registration deletions before updating the community config (the supabase
.from("community_configs").update call that sets navigation_config and
updated_at) can leave the system inconsistent if the final update fails; modify
the flow to either wrap all DB changes and related cleanup in a single
transaction (use Supabase/Postgres transaction via rpc/BEGIN...COMMIT or
server-side function) or perform the config update first (the .update/select on
community_configs using updateRequest.communityId and is_published=true) and
confirm it succeeded before performing storage deletions and space registration
removals, or implement soft-delete flags for cleanup steps so failures are
reversible — update code paths that call storage deletion and space registration
deletion to run only after the update returns no updateError and updatedRows are
present, or move deletions into a transactional stored procedure if possible.
🧹 Nitpick comments (17)
src/app/page.tsx (1)

15-21: Simplify the href normalization logic.

The ternary on lines 17-19 is correct but slightly verbose. The startsWith('/') check keeps the href unchanged when true, which means you're just ensuring a leading slash exists.

♻️ Suggested simplification
   if (firstNavItem?.href) {
-    // Remove leading slash if present and redirect
-    const href = firstNavItem.href.startsWith('/') 
-      ? firstNavItem.href 
-      : `/${firstNavItem.href}`;
+    // Ensure leading slash and redirect
+    const href = firstNavItem.href.startsWith('/') 
+      ? firstNavItem.href 
+      : `/${firstNavItem.href}`;
     redirect(href);
   }

The logic is correct; the comment says "Remove leading slash" but the code actually ensures one exists. Consider updating the comment to match the behavior.

src/common/components/organisms/navigation/errorHandling.ts (2)

19-28: String-based error classification is fragile.

The error type detection using includes('Invalid'), includes('duplicate'), includes('admin'), and includes('permission') could produce false positives if these strings appear in unrelated error messages (e.g., "Invalid token" being classified as VALIDATION when it might be an auth error).

Consider using more specific patterns, error codes, or a dedicated error class hierarchy for more reliable classification.

♻️ Suggested approach
// Option 1: Use error codes or custom error classes
class ValidationError extends Error {
  readonly code = 'VALIDATION_ERROR';
}

// Option 2: More specific string patterns
const VALIDATION_PATTERNS = [
  /^Invalid\s+(href|label|name)/i,
  /duplicate\s+(href|name)/i,
];

const PERMISSION_PATTERNS = [
  /not\s+an?\s+admin/i,
  /permission\s+denied/i,
];

30-38: Consider extending retryable error detection.

The current logic only marks 5xx errors as retryable. Consider also handling:

  • Timeout errors (error.code === 'ECONNABORTED' or error.code === 'ETIMEDOUT')
  • Rate limiting (429 status)
  • Network connectivity issues (error.code === 'ERR_NETWORK')
♻️ Extended retryable detection
     if (axios.isAxiosError(error)) {
-      const isRetryable = error.response?.status !== undefined && error.response.status >= 500;
+      const status = error.response?.status;
+      const isServerError = status !== undefined && status >= 500;
+      const isRateLimited = status === 429;
+      const isNetworkError = error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED';
+      const isRetryable = isServerError || isRateLimited || isNetworkError;
       return {
         type: 'NETWORK',
         message: error.response?.data?.error?.message || error.message || 'Network error occurred',
         retryable: isRetryable,
       };
     }
src/common/components/atoms/editable-text.tsx (1)

25-26: Consider using camelCase for state setters.

The state setter names setisEditing and settext deviate from the conventional camelCase pattern (setIsEditing, setText). While functional, consistent naming improves readability.

♻️ Suggested fix
-  const [isEditing, setisEditing] = useState(false);
-  const [text, settext] = useState(initialText);
+  const [isEditing, setIsEditing] = useState(false);
+  const [text, setText] = useState(initialText);

Then update all references to use the new names.

src/config/loaders/registry.ts (1)

129-135: Consider documenting or adjusting the cleanup threshold.

The magic number 10 for the cache size threshold before triggering cleanup is reasonable but arbitrary. A few observations:

  1. Cleanup only runs on writes, so a read-heavy workload could accumulate expired entries until the next write
  2. The threshold of 10 is low—consider whether this matches expected community count in production

This is fine for now given the short TTL (60s) and the fact that expired entries will be individually purged on read miss.

💡 Optional: Extract threshold to a named constant
 const SYSTEM_CONFIG_CACHE_TTL_MS = 60_000;
+const SYSTEM_CONFIG_CACHE_CLEANUP_THRESHOLD = 10;

 // ...

 function writeSystemConfigCache(communityId: string, value: SystemConfig | null) {
-  if (SYSTEM_CONFIG_CACHE.size > 10) {
+  if (SYSTEM_CONFIG_CACHE.size > SYSTEM_CONFIG_CACHE_CLEANUP_THRESHOLD) {
     cleanupExpiredCacheEntries();
   }
src/common/components/organisms/navigation/useNavigation.ts (1)

65-66: Unused store selectors deleteNavigationItem and renameNavigationItem.

These selectors are destructured from the store but never used within this hook. They should be removed to avoid confusion.

♻️ Suggested fix
   const {
     loadNavigation,
     localNavigation,
     hasUncommittedChanges,
     createNavigationItem,
-    deleteNavigationItem,
-    renameNavigationItem,
     updateNavigationOrder,
     commitNavigationChanges,
     resetNavigationChanges,
   } = useAppStore((state) => ({
     loadNavigation: state.navigation.loadNavigation,
     localNavigation: state.navigation.localNavigation,
     hasUncommittedChanges: state.navigation.hasUncommittedChanges,
     createNavigationItem: state.navigation.createNavigationItem,
-    deleteNavigationItem: state.navigation.deleteNavigationItem,
-    renameNavigationItem: state.navigation.renameNavigationItem,
     updateNavigationOrder: state.navigation.updateNavigationOrder,
     commitNavigationChanges: state.navigation.commitNavigationChanges,
     resetNavigationChanges: state.navigation.resetNavigationChanges,
   }));
src/common/components/organisms/navigation/NavigationEditor.tsx (4)

103-111: Icon button refs are never cleaned up.

The iconButtonRefs.current object accumulates refs for each item but never removes them when items are deleted. This could lead to memory leaks over time if many items are created and deleted.

♻️ Suggested cleanup approach

Consider cleaning up refs when items are removed, or use a WeakMap-like pattern. Alternatively, since refs are lightweight and items are typically few, this may be acceptable for this use case.

+  // Clean up stale refs when editableItems changes
+  useEffect(() => {
+    const currentIds = new Set(editableItems.map(item => item.id));
+    Object.keys(iconButtonRefs.current).forEach(id => {
+      if (!currentIds.has(id)) {
+        delete iconButtonRefs.current[id];
+      }
+    });
+  }, [editableItems]);

151-166: Error re-throwing after toast could cause double error display.

When onRename throws, the code shows a toast error (line 160) and then re-throws (lines 163-165). If an error boundary catches this, it may show another error UI, resulting in duplicate error feedback to the user.

♻️ Consider not re-throwing after showing toast
       toast.error(errorMessage);
-      
-      // Re-throw for error boundaries
-      if (error instanceof Error) {
-        throw error;
-      }
+      // Error already handled with toast - no need to re-throw

Or, if error boundary handling is needed, don't show the toast here and let the boundary handle it.


178-179: Orphaned JSDoc comment block.

Lines 169-178 contain an orphaned JSDoc comment that describes an effect for URL updates, but no such effect exists in the code. This appears to be leftover documentation from removed code.

♻️ Remove orphaned comment
-  /**
-   * Updates URL when the currently viewed navigation item's href changes
-   * 
-   * This effect watches for changes in localNavigation and updates the browser URL
-   * if the current pathname matches an item that has been renamed. This ensures
-   * the URL stays in sync with navigation item changes without requiring a page reload.
-   * 
-   * Uses a ref to track the previous pathname to avoid unnecessary updates and
-   * prevent race conditions where the effect might run before the store has fully updated.
-   */

   /**
    * Handles renaming a navigation item

371-401: Delete button shows success toast before deletion completes.

The delete button calls onDelete(item.id) and immediately shows toast.success("Navigation item deleted"). If onDelete is asynchronous or throws, the success toast would be misleading.

♻️ Suggested fix

Move the toast to after the delete operation, or handle it in the parent component that owns the delete logic.

                     onClick={(e) => {
                       e.preventDefault();
                       e.stopPropagation();
                       console.log('[NavigationEditor] Deleting navigation item:', {
                         itemId: item.id,
                         label: item.label,
                         href: item.href
                       });
                       onDelete(item.id);
-                      toast.success("Navigation item deleted");
                     }}

Note: The toast is also shown in Navigation.tsx's handleDeleteItem, so removing it here would be appropriate to avoid duplication.

src/common/components/organisms/Navigation.tsx (2)

500-500: Redundant ternary for username prop.

Line 500 has username={username ? username : undefined} which is equivalent to just username={username} since username is already typed as string | undefined (from line 276).

♻️ Simplify
-                  username={username ? username : undefined}
+                  username={username}

597-612: Edit Nav button creates inline Icon component.

Line 601-605 creates an inline arrow function component for the icon, which will cause React to treat it as a new component on each render. This could cause unnecessary re-renders of the NavigationButton.

♻️ Extract to a named component
+const EditNavIcon: React.FC = () => (
+  <div className="w-6 h-6 flex items-center justify-center">
+    <FaPencil className="w-4 h-4 text-gray-800 dark:text-white" />
+  </div>
+);

// In the render:
                 {isNavigationEditable && !mobile && (
                   <NavigationButton
                     label={navEditMode ? "Exit Edit" : "Edit Nav"}
-                    Icon={navEditMode ? LogoutIcon : () => (
-                      <div className="w-6 h-6 flex items-center justify-center">
-                        <FaPencil className="w-4 h-4 text-gray-800 dark:text-white" />
-                      </div>
-                    )}
+                    Icon={navEditMode ? LogoutIcon : EditNavIcon}
src/pages/api/navigation/config.ts (1)

82-92: Sensitive data logged in production.

The debug logging on lines 83-92 logs navigation items including spaceId and potentially other data. While this is helpful for debugging, ensure these logs are appropriate for production environments or consider gating them behind a development check.

♻️ Gate debug logging
-  // Debug: log what we received
-  console.log('[API] Received navigation items:', {
-    communityId: updateRequest.communityId,
-    itemCount: updateRequest.navigationConfig.items?.length || 0,
-    items: updateRequest.navigationConfig.items?.map((item: NavigationItem) => ({
-      id: item.id,
-      label: item.label,
-      href: item.href,
-      spaceId: item.spaceId
-    }))
-  });
+  if (process.env.NODE_ENV === 'development') {
+    console.log('[API] Received navigation items:', {
+      communityId: updateRequest.communityId,
+      itemCount: updateRequest.navigationConfig.items?.length || 0,
+      items: updateRequest.navigationConfig.items?.map((item: NavigationItem) => ({
+        id: item.id,
+        label: item.label,
+        href: item.href,
+        spaceId: item.spaceId
+      }))
+    });
+  }
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (1)

72-126: Memoization dependency array includes providedTabName but only affects local branch.

The spaceData useMemo includes providedTabName in its dependency array, but providedTabName is only used inside the if (localNavItem && localNavItem.spaceId) branch to compute activeTab. When server data is used (lines 112-114), the memoization will unnecessarily recompute even though the result is the same serverSpaceData reference.

This is a minor inefficiency but acceptable given the complexity of refactoring. Consider extracting the local computation to avoid recomputing when only providedTabName changes and server data is in use.

src/common/data/stores/app/navigation/navigationStore.ts (3)

445-494: No retry logic for transient failures during space registration.

The space registration loop fails the entire commit on the first error. For transient network issues, this could be frustrating for users who have made many changes. Consider adding retry logic with exponential backoff for network errors, while still failing fast on validation errors.

#!/bin/bash
# Check if there's a retry utility already available in the codebase
rg -n "retry" --type ts -l | head -10

453-453: Redundant non-null assertion after explicit check.

Line 453 uses registrationResponse.value!.spaceId but line 449 already checks !registrationResponse.value?.spaceId. After the check, TypeScript should know value is defined. The ! is unnecessary and could mask future type issues.

♻️ Suggested fix
-const actualSpaceId = registrationResponse.value!.spaceId;
+const actualSpaceId = registrationResponse.value.spaceId;

501-526: Multiple individual set calls in a loop may cause excessive re-renders.

The loop at lines 501-526 calls set for each successful registration. With many new items, this triggers multiple state updates. Consider batching these updates into a single set call.

♻️ Suggested fix: Batch state updates
-for (const registration of successfulRegistrations) {
-  set((draft) => {
+set((draft) => {
+  for (const registration of successfulRegistrations) {
     const itemIndex = draft.navigation.localNavigation.findIndex(
       (item) => item.id === registration.itemId
     );
     if (itemIndex !== -1) {
       draft.navigation.localNavigation[itemIndex].spaceId = registration.actualSpaceId;
     }
     // Ensure the spaceId is set correctly in localSpaces
     if (draft.space.localSpaces[registration.spaceId]) {
       if (registration.spaceId !== registration.actualSpaceId) {
         // Move the localSpaces entry to the actual spaceId (shouldn't happen, but handle it)
         console.log('[navigationStore] Moving localSpace entry:', {
           from: registration.spaceId,
           to: registration.actualSpaceId
         });
         draft.space.localSpaces[registration.actualSpaceId] = draft.space.localSpaces[registration.spaceId];
         draft.space.localSpaces[registration.actualSpaceId].id = registration.actualSpaceId;
         delete draft.space.localSpaces[registration.spaceId];
       } else {
         // Ensure the spaceId is set correctly
         draft.space.localSpaces[registration.actualSpaceId].id = registration.actualSpaceId;
       }
     }
-  }, "commitNavigationChanges-updateSpaceId");
-}
+  }
+}, "commitNavigationChanges-updateSpaceIds");
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b775ca and 612f095.

📒 Files selected for processing (21)
  • docs/DOCUMENTATION_OVERVIEW.md
  • docs/README.md
  • docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md
  • docs/SYSTEMS/NAVIGATION/OVERVIEW.md
  • scripts/seed.ts
  • src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx
  • src/app/page.tsx
  • src/common/components/atoms/editable-text.tsx
  • src/common/components/atoms/reorderable-tab.tsx
  • src/common/components/organisms/Navigation.tsx
  • src/common/components/organisms/navigation/NavigationEditor.tsx
  • src/common/components/organisms/navigation/NavigationItem.tsx
  • src/common/components/organisms/navigation/errorHandling.ts
  • src/common/components/organisms/navigation/useNavigation.ts
  • src/common/data/stores/app/navigation/navigationStore.ts
  • src/common/lib/theme/ThemeSettingsEditor.tsx
  • src/config/index.ts
  • src/config/loaders/registry.ts
  • src/config/systemConfig.ts
  • src/pages/api/navigation/config.ts
  • tests/navigation/errorHandling.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/navigation/errorHandling.test.ts
  • docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-31T21:13:36.931Z
Learnt from: willyogo
Repo: blankdotspace/space-system PR: 1641
File: src/fidgets/farcaster/components/CastRow.tsx:247-250
Timestamp: 2025-12-31T21:13:36.931Z
Learning: In src/fidgets/farcaster/components/CastRow.tsx (and similar UI components), hardcoded semi-transparent colors like `rgba(128, 128, 128, 0.2)` for borders and backgrounds are acceptable because they adapt to any background color, eliminating the need for theme tokens in ui_config for these specific use cases.

Applied to files:

  • src/common/components/organisms/Navigation.tsx
🧬 Code graph analysis (10)
src/common/lib/theme/ThemeSettingsEditor.tsx (1)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (55-57)
src/common/components/atoms/reorderable-tab.tsx (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)
  • renameTab (375-497)
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (4)
src/common/types/spaceData.ts (2)
  • NavPageSpacePageData (77-83)
  • SPACE_TYPES (7-13)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/common/data/stores/app/index.tsx (1)
  • useAppStore (174-174)
src/app/(spaces)/PublicSpace.tsx (1)
  • PublicSpace (33-463)
src/pages/api/navigation/config.ts (7)
src/config/systemConfig.ts (1)
  • NavigationConfig (144-149)
src/common/lib/signedFiles.ts (3)
  • Signable (20-23)
  • isSignable (29-41)
  • validateSignable (43-52)
src/common/data/api/requestHandler.ts (2)
  • NounspaceResponse (9-16)
  • requestHandler (31-51)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/utils/navUtils.ts (2)
  • validateNavItemLabel (19-35)
  • validateNavItemHref (42-72)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/supabase/database.d.ts (1)
  • Json (3-9)
src/common/components/organisms/Navigation.tsx (10)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (55-57)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/common/lib/theme/index.d.ts (1)
  • UserTheme (7-23)
src/common/components/organisms/navigation/useNavigation.ts (1)
  • useNavigation (41-318)
src/common/data/stores/app/index.tsx (1)
  • useAppStore (174-174)
src/config/systemConfig.ts (1)
  • NavigationItem (156-164)
src/common/components/organisms/navigation/ErrorBoundary.tsx (1)
  • NavigationErrorBoundary (22-113)
src/common/components/organisms/navigation/NavigationEditor.tsx (1)
  • NavigationEditor (494-494)
src/common/lib/utils/analyticsUtils.ts (1)
  • trackAnalyticsEvent (5-12)
src/common/components/atoms/button.tsx (1)
  • Button (81-81)
src/common/components/organisms/navigation/useNavigation.ts (5)
src/common/components/organisms/navigation/NavigationItem.tsx (1)
  • NavigationItem (146-160)
src/config/systemConfig.ts (2)
  • NavigationItem (156-164)
  • SystemConfig (27-37)
src/common/components/organisms/navigation/constants.ts (1)
  • NAVIGATION_REORDER_DEBOUNCE_MS (14-14)
src/config/loaders/registry.ts (1)
  • DEFAULT_COMMUNITY_ID (6-6)
src/common/components/organisms/navigation/errorHandling.ts (2)
  • normalizeNavigationError (18-48)
  • getUserFriendlyMessage (56-69)
src/common/components/atoms/editable-text.tsx (1)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/common/data/stores/app/navigation/navigationStore.ts (2)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/common/utils/navUtils.ts (6)
  • validateNavItemLabel (19-35)
  • isDuplicateNavLabel (82-90)
  • generateUniqueNavLabel (164-179)
  • validateNavItemHref (42-72)
  • isDuplicateNavHref (99-108)
  • generateUniqueHrefFromLabel (118-154)
src/config/loaders/registry.ts (1)
src/config/systemConfig.ts (1)
  • SystemConfig (27-37)
src/common/components/organisms/navigation/NavigationEditor.tsx (5)
src/common/components/organisms/navigation/NavigationItem.tsx (1)
  • NavigationItem (146-160)
src/common/lib/utils/mergeClasses.ts (1)
  • mergeClasses (4-6)
src/common/utils/navUtils.ts (1)
  • validateNavItemLabel (19-35)
src/common/components/organisms/navigation/constants.ts (1)
  • NAVIGATION_MAX_LABEL_LENGTH (23-23)
src/common/lib/utils/analyticsUtils.ts (1)
  • trackAnalyticsEvent (5-12)
🔇 Additional comments (33)
src/common/components/atoms/reorderable-tab.tsx (1)

76-82: LGTM! Good defensive check for optional renameTab prop.

Adding && renameTab to the condition ensures EditableText is only rendered when the callback is actually provided, preventing potential runtime errors when the component is used without rename functionality.

src/common/lib/theme/ThemeSettingsEditor.tsx (3)

31-32: LGTM!

The imports are correctly added and both are used in the component.


434-452: LGTM!

The conditional rendering correctly handles the nav edit mode case:

  • Theme changes are saved locally (staged) without committing.
  • User returns to navigation edit mode where they can commit or cancel all changes together.

This aligns well with the PR objective of staging space edits locally during nav edit mode.


189-191: Verify that this component is always rendered within SidebarContext.Provider.

useSidebarContext() directly returns useContext(SidebarContext) without a default fallback. If ThemeSettingsEditor is ever rendered outside the sidebar context provider, setEditMode will be undefined and calling it at line 443 will throw a runtime error.

Consider adding a guard or ensuring the context provides safe defaults.

#!/bin/bash
# Check where ThemeSettingsEditor is rendered and if it's within SidebarContext
rg -n "ThemeSettingsEditor" --type=tsx --type=ts -C 3
src/app/page.tsx (1)

23-26: LGTM on the fallback redirect logic.

The fallback to /home when no navigation items exist is a sensible default. The return null after redirect() is fine for TypeScript inference even though it's technically unreachable.

docs/DOCUMENTATION_OVERVIEW.md (1)

31-32: LGTM!

The navigation system documentation entries are consistent with the existing documentation structure and properly reference the new SYSTEMS/NAVIGATION/OVERVIEW.md file introduced by this PR.

Also applies to: 74-74

src/common/components/organisms/navigation/errorHandling.ts (1)

56-68: LGTM on getUserFriendlyMessage.

The function exhaustively handles all discriminated union cases, and TypeScript will catch any missing cases. The messaging is appropriate for user display.

src/common/components/atoms/editable-text.tsx (3)

28-33: LGTM on the sync effect.

The effect correctly synchronizes the internal text state with initialText only when not editing, preventing external updates from interrupting user input.


35-52: LGTM on validation and edit completion logic.

The validation flow is well-structured:

  • Trims input before validation
  • Falls back to validateTabName when no custom validator is provided
  • Resets text on validation failure without exiting edit mode
  • Only calls updateMethod on successful validation

68-76: Good event propagation handling for edit mode.

The onClick and onDoubleClick handlers with stopPropagation() and preventDefault() correctly prevent navigation and text selection interference while the input is active.

scripts/seed.ts (1)

20-20: Environment variable rename is complete and consistent across the codebase.

The rename from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY has been properly applied in all relevant locations:

  • Source code (src/common/data/database/supabase/clients/server.ts)
  • Tests (tests/setup.ts)
  • Scripts (scripts/seed.ts - lines 20, 64, 70)
  • Environment template (.env.example)
  • Documentation (docs/GETTING_STARTED.md, docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md)

No occurrences of the old variable name remain.

src/config/index.ts (1)

72-76: LGTM! Log level change is appropriate.

Downgrading from console.error to console.warn is correct for these fallback paths. Since the system gracefully recovers by using DEFAULT_COMMUNITY_ID, these are expected scenarios rather than true errors. This reduces noise in error monitoring while still maintaining visibility.

Also applies to: 88-94

src/config/systemConfig.ts (2)

35-37: LGTM! Adding communityId to SystemConfig.

This field is necessary for the navigation store to correctly scope API calls to the appropriate community. The value is populated during config loading in transformRowToSystemConfig.


160-160: Acceptable trade-off: Widening icon type from union to string.

Changing from a constrained union type to a generic string reduces type safety but is necessary to support dynamic react-icons names (e.g., 'FaHouse', 'FaRss') and custom icon URLs. The comment adequately documents the expected values.

Consider adding runtime validation in the icon rendering component to handle invalid icon names gracefully.

docs/SYSTEMS/NAVIGATION/OVERVIEW.md (1)

1-302: Well-structured documentation covering the navigation system.

The documentation provides a clear overview of the navigation editor architecture including data flow, components, API contracts, and security considerations. The local-first pattern explanation and memory management notes are particularly helpful for maintainers.

src/config/loaders/registry.ts (3)

83-95: LGTM! Signature change to include communityId.

The updated signature correctly propagates communityId into the returned SystemConfig, aligning with the interface definition in systemConfig.ts. This enables downstream code (navigation store, API calls) to access the community context.


97-108: Cache cleanup implementation looks good.

The cleanup function correctly iterates and removes expired entries. The O(n) iteration is acceptable given the small cache size (capped implicitly by the trigger threshold).


251-252: LGTM! Call site correctly passes communityId.

The call to transformRowToSystemConfig(data, communityId) properly supplies the community ID that was used for the database query, ensuring consistency.

src/common/components/organisms/navigation/NavigationItem.tsx (2)

146-160: Memo comparator omits Icon and onClick/onNavigate props.

The custom memo comparator doesn't check Icon, onClick, or onNavigate props. If these change (e.g., different icon component or callback identity), the component won't re-render. This could lead to stale behavior. Consider if these omissions are intentional for performance.


231-243: Memo comparator omits onClick prop.

Similar to NavigationItem, the NavigationButton memo comparator doesn't include onClick. If the callback identity changes, the button won't re-render with the new handler.

src/common/components/organisms/navigation/useNavigation.ts (2)

126-145: Potential race condition in cancel navigation effect.

If the user triggers multiple cancel operations quickly, or if the navigation takes longer than expected, the pendingCancelTargetRef could be overwritten. Additionally, if the target URL changes during navigation (e.g., due to redirects), the condition pathname === pendingCancelTargetRef.current might never match, leaving the cancel operation incomplete.

Consider adding a timeout fallback or tracking navigation state more robustly.


308-317: This approach is correct and does not create stale values.

The hasUncommittedChanges() function reads current state via get().navigation on every call, returning a fresh boolean at the moment the hook renders. Since the selector includes localNavigation (line 62), the component re-renders whenever localNavigation changes—which is the primary condition affecting the result of hasUncommittedChanges(). Each render calls the function with up-to-date state, so the value is not stale.

src/common/components/organisms/navigation/NavigationEditor.tsx (2)

94-99: Large icon pack merged on every render's memoization.

The iconPack memo has an empty dependency array, which is correct for stability. However, merging three large icon libraries (FaIcons, BsIcons, GiIcons) creates a substantial object. This is fine since it's memoized, but be aware this increases bundle size significantly.


239-241: router.replace(newUrl) with potentially null newUrl.

The variable newUrl is typed as string | null (line 223), but TypeScript should catch this since the if (newUrl) check on line 234 guards the usage. However, the startTransition callback captures newUrl in a closure, and TypeScript's type narrowing doesn't persist into the callback. This should work at runtime but could be cleaner.

src/common/components/organisms/Navigation.tsx (1)

485-519: NavigationEditor error boundary onError has a TODO for production logging.

The error boundary's onError callback has a TODO comment for integrating with an error logging service. This is fine for now, but ensure this is tracked for follow-up.

src/pages/api/navigation/config.ts (2)

191-249: Space deletion failures are silently logged but don't affect response.

When deleting spaces for removed navigation items, failures are logged with console.warn but the operation continues. This is intentional (as noted in the comment on line 245-246), but there's no way for the client to know if cleanup partially failed. Consider returning warnings in the response or implementing a cleanup job.


269-271: JSON serialization round-trip for type compatibility.

The JSON.parse(JSON.stringify(...)) pattern on line 271 is used to ensure JSON compatibility with Supabase's Json type. While this works, it has performance overhead and could lose certain JavaScript values (like undefined becoming null). This is acceptable here since navigation config should be JSON-safe.

src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (4)

88-92: Potential edge case: empty order array fallback.

When localSpace.order is truthy but empty ([]), the condition localSpace.order && localSpace.order.length > 0 correctly falls back to "Home". However, this hardcoded fallback should align with the store's initial tab name.

Based on navigationStore.ts (line 174), the initial default tab is also "Home", so this is consistent.


147-160: 404 UI implementation is appropriate for client components.

The comment correctly notes that notFound() cannot be used in client components. The inline 404 UI is a reasonable fallback. Consider extracting this to a reusable component if similar patterns exist elsewhere.


136-136: Tab name encoding is correctly applied.

Using encodeURIComponent(tabName) for the URL path ensures special characters in tab names are properly escaped.


96-107: Local space data construction is intentional for uncommitted changes.

When a local navigation item is found, space data is reconstructed from local stores with spaceOwnerFid: undefined and config: INITIAL_SPACE_CONFIG_EMPTY. This behavior is by design—the code prioritizes local (uncommitted) changes over remote data to ensure users see their edits. For existing server-side items being edited locally, this reconstruction is the expected behavior. If these defaults are problematic for the edit flow, consider preserving spaceOwnerFid and config from the original server data when available.

src/common/data/stores/app/navigation/navigationStore.ts (2)

1-19: Imports are well-organized and appropriate.

The imports cover necessary utilities, types, and external libraries. Using named imports from lodash (cloneDeep) is good for tree-shaking.


84-94: This concern is not applicable. The code already protects against overwriting local changes through dual guards in useNavigation.ts (line 84): the guard checks both !hasLoadedNavigationRef.current and localNavigation.length === 0, ensuring loadNavigation runs only once on initial mount when the store is empty. The function cannot overwrite uncommitted edits.

Likely an incorrect or invalid review comment.

Comment thread docs/README.md
Comment thread docs/SYSTEMS/NAVIGATION/OVERVIEW.md
Comment thread src/common/components/organisms/Navigation.tsx Outdated
Comment on lines +92 to +102
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (e.key === " ") {
// Prevent page scroll on Space
e.preventDefault();
}
handleClick(e as unknown as React.MouseEvent<HTMLAnchorElement>);
}
},
[disable, handleClick]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Redundant e.preventDefault() call and unsafe type cast.

Line 96 calls e.preventDefault() again after it was already called on line 93. Additionally, the type cast on line 98 (as unknown as React.MouseEvent) is unsafe and could cause issues since keyboard events don't have properties like button, metaKey, etc. that handleClick checks.

🛠️ Suggested fix
   const handleKeyDown = useCallback(
     (e: React.KeyboardEvent<HTMLAnchorElement>) => {
       if (disable) {
         e.preventDefault();
         return;
       }

       // Enter or Space key activates the link
       if (e.key === "Enter" || e.key === " ") {
         e.preventDefault();
-        if (e.key === " ") {
-          // Prevent page scroll on Space
-          e.preventDefault();
-        }
-        handleClick(e as unknown as React.MouseEvent<HTMLAnchorElement>);
+        if (!openInNewTab && href && href.startsWith("/")) {
+          router.push(href);
+          React.startTransition(() => {
+            onClick?.();
+            onNavigate?.();
+          });
+        } else {
+          onClick?.();
+          onNavigate?.();
+        }
       }
     },
-    [disable, handleClick]
+    [disable, href, openInNewTab, router, onClick, onNavigate]
   );
🤖 Prompt for AI Agents
In @src/common/components/organisms/navigation/NavigationItem.tsx around lines
92 - 102, Remove the duplicate e.preventDefault() and stop casting the keyboard
event to a MouseEvent; instead make handleClick accept a union event type (e.g.,
React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>)
and update its internals to guard any mouse-only properties (use type guards or
checks like 'button' in event and optional chaining for metaKey/ctrlKey) before
accessing them, then call handleClick(e) directly from the onKeyDown handler
when Enter or Space is pressed.

Comment thread src/common/data/stores/app/navigation/navigationStore.ts Outdated
Comment on lines +337 to +345
return state.localNavigation.some((localItem, index) => {
const remoteItem = state.remoteNavigation[index];
return !remoteItem ||
localItem.id !== remoteItem.id ||
localItem.label !== remoteItem.label ||
localItem.href !== remoteItem.href ||
localItem.icon !== remoteItem.icon ||
localItem.spaceId !== remoteItem.spaceId;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon comparison uses reference equality, which may cause false positives.

The hasUncommittedChanges function compares localItem.icon !== remoteItem.icon. If icon is an object (e.g., { type: 'react-icon', name: 'FaHome' }), this reference comparison will always return true even when the values are identical, causing phantom "uncommitted changes" detection.

🔧 Suggested fix: Use deep comparison for icon
+import { isEqual } from "lodash";

 hasUncommittedChanges: () => {
   const state = get().navigation;
   // Compare arrays directly - check length, order, and content
   if (state.localNavigation.length !== state.remoteNavigation.length) {
     return true;
   }
   
   // Check if order changed or any item properties changed
   return state.localNavigation.some((localItem, index) => {
     const remoteItem = state.remoteNavigation[index];
     return !remoteItem || 
       localItem.id !== remoteItem.id ||
       localItem.label !== remoteItem.label ||
       localItem.href !== remoteItem.href ||
-      localItem.icon !== remoteItem.icon ||
+      !isEqual(localItem.icon, remoteItem.icon) ||
       localItem.spaceId !== remoteItem.spaceId;
   });
 },
🤖 Prompt for AI Agents
In @src/common/data/stores/app/navigation/navigationStore.ts around lines 337 -
345, The icon field is being compared by reference in hasUncommittedChanges
(state.localNavigation / localItem.icon vs state.remoteNavigation /
remoteItem.icon), which yields false positives for equivalent icon objects;
replace the reference check localItem.icon !== remoteItem.icon with a deep
equality check (e.g., use a stable deep-equal utility like lodash/isEqual or a
project deepEqual helper) and guard for null/undefined so two absent icons are
considered equal, keeping the rest of the property comparisons unchanged.

Comment on lines +565 to +568

// Update navigation config in database
console.log('[navigationStore] Sending navigation config update request');
await axiosBackend.put("/api/navigation/config", signedRequest);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check the navigationStore.ts file directly
echo "=== Content at lines 565-568 ==="
sed -n '560,575p' src/common/data/stores/app/navigation/navigationStore.ts

echo -e "\n=== Search for axiosBackend definition and imports ==="
rg -n "import.*axiosBackend|export.*axiosBackend" src/common/data/stores/app/navigation/navigationStore.ts

echo -e "\n=== axiosBackend usage and context ==="
rg -n "axiosBackend" --type ts -B 2 -A 5 | head -80

Repository: blankdotspace/space-system

Length of output: 6033


🏁 Script executed:

#!/bin/bash
# Find and read the axiosBackend configuration file
echo "=== Searching for axiosBackend definition ==="
find . -path ./node_modules -prune -o -type f -name "backend.ts" -o -name "backend.js" | grep -E "data/api"

echo -e "\n=== Reading the backend API configuration ==="
cat -n src/common/data/api/backend.ts 2>/dev/null || cat -n src/common/data/api/backend.js

echo -e "\n=== Context around the axiosBackend.put call in navigationStore.ts ==="
sed -n '555,580p' src/common/data/stores/app/navigation/navigationStore.ts

Repository: blankdotspace/space-system

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Find the function containing the axiosBackend.put call
echo "=== Finding function context around line 573 ==="
sed -n '540,600p' src/common/data/stores/app/navigation/navigationStore.ts | cat -n

echo -e "\n=== Check for try-catch around this block ==="
sed -n '520,595p' src/common/data/stores/app/navigation/navigationStore.ts | cat -n

Repository: blankdotspace/space-system

Length of output: 7004


🏁 Script executed:

#!/bin/bash
# Check if there are other axiosBackend.put calls that validate response
echo "=== Other axiosBackend.put calls to see response validation patterns ==="
rg -n "axiosBackend\.put" --type ts -A 3 -B 1

echo -e "\n=== Check if API endpoints validate responses elsewhere ==="
rg -n "validateStatus|response\.data|error.*response" src/common/data/stores/app/navigation/navigationStore.ts

Repository: blankdotspace/space-system

Length of output: 1216


Add response data validation for navigation config PUT request.

The axiosBackend.put call does not validate the response data after a successful HTTP response. While the call is wrapped in a try-catch that handles HTTP errors, if the backend returns a 2xx status with an error payload in the response body, the update will be treated as successful and state will be updated. This is inconsistent with the pattern used elsewhere in the codebase (e.g., line 484) where response.data?.error?.message is validated. Consider validating the response to ensure the update actually succeeded on the server.

🤖 Prompt for AI Agents
In @src/common/data/stores/app/navigation/navigationStore.ts around lines 565 -
568, The axiosBackend.put call in navigationStore that sends signedRequest
should validate the returned response body before treating the update as
successful: capture the result of await
axiosBackend.put("/api/navigation/config", signedRequest), check response.data
for an error (e.g., response.data?.error?.message) or an explicit success flag,
and if an error is present throw or handle it the same way as the pattern used
at the other call (line ~484) so the store does not update state on 2xx error
payloads; update the console log block around navigationStore to use the
validated response and only proceed when the payload indicates success.

} else if (error?.message) {
console.error("Error message:", error.message);
}
console.error("Full error object:", JSON.stringify(error, null, 2));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Logging full error object with JSON.stringify may fail on circular references.

JSON.stringify(error, null, 2) will throw if the error object contains circular references, which is common with Axios errors. This could mask the original error.

🔧 Suggested fix: Use safe serialization
-console.error("Full error object:", JSON.stringify(error, null, 2));
+try {
+  console.error("Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
+} catch {
+  console.error("Full error object (could not stringify):", error);
+}
📝 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
console.error("Full error object:", JSON.stringify(error, null, 2));
try {
console.error("Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
} catch {
console.error("Full error object (could not stringify):", error);
}
🤖 Prompt for AI Agents
In @src/common/data/stores/app/navigation/navigationStore.ts at line 596, The
current console.error call uses JSON.stringify(error, null, 2) which can throw
on circular structures (common with Axios errors); replace it with a safe
serialization approach: either use Node's util.inspect(error, { depth: null })
or wrap JSON.stringify in try/catch and fall back to error.toString() or logging
specific fields (error.message, error.stack, error.response?.data) so the
logging line that references error (the console.error("Full error object:",
JSON.stringify(error, null, 2)) statement) never throws and always outputs
useful error details.

Comment thread src/pages/api/navigation/config.ts
Comment thread src/pages/api/navigation/config.ts
@j-paterson j-paterson marked this pull request as ready for review January 13, 2026 00:39
Add visual feedback when committing navigation changes by showing a
spinning spinner icon and "Committing..." text while the commit
operation is in progress.

Changes:
- Add isCommitting state to useNavigation hook
- Set loading state during commit operation (in finally block)
- Pass isCommitting prop to NavigationEditor component
- Update commit button to show FaSpinner with "Committing..." when loading
- Disable button while committing to prevent multiple submissions
- Add disabled styles for visual feedback

This provides clear feedback to users that their commit is being
processed and prevents accidental duplicate commits.
- Fix React hooks error in NavPageSpace: move all hooks before conditional returns
  to comply with Rules of Hooks
- Add comprehensive logging throughout navigation codebase for debugging:
  - useNavigation: commit, cancel, create, reorder operations
  - navigationStore: load, create, delete, rename, updateOrder, commit operations
  - NavigationEditor: rename, delete operations
  - API endpoint: request validation, config fetch, update operations
- Fix navigation cancel 404 flash: use usePathname to wait for navigation completion
  before resetting localNavigation, keeping uncommitted items available during
  navigation transition
- Extract error handling to dedicated module (errorHandling.ts) for reuse
- Add useEffect to automatically expand sidebar when navEditMode becomes true
- Hide sidebar toggle button during navigation edit mode (cleaner UX)
- Prevent sidebar collapse while editing navigation to ensure all controls are visible
- Separate cancel button from commit button condition so it's always visible
- Allow users to exit edit mode even when no changes have been made
- Commit button only appears when there are uncommitted changes
- Cancel button takes full width when commit button is hidden for better UX
- Integrate IconSelector component for navigation items (same system as mobile editor)
- Update NavigationItem icon type to support react-icons names and custom URLs
- Update iconFor function to handle react-icons names and custom icon URLs
- Fix icon sizing in edit mode (w-5 h-5) to match standard nav visual size
- Add back button to space customization when in nav edit mode
- Space changes are staged locally when navigating back from space edit in nav edit mode
- Changes are not committed until nav commits to maintain data consistency
…hanges

- Add confirmation dialog before canceling navigation edits when changes exist
- Skip confirmation dialog when no uncommitted changes (direct exit)
- Show full-width 'Exit Edit Mode' button when no changes to save
- Extract cancel handlers (handleCancelClick, handleConfirmedCancel) for cleaner code
- Flatten conditional rendering for better readability
- Reset confirmation state when exiting edit mode
- Add communityId to SystemConfig interface and pass through from database
- Update useNavigation to use systemConfig.communityId with DEFAULT_COMMUNITY_ID fallback
- Update React.memo comparisons in NavigationItem to use communityId instead of community?.type
- Add cache cleanup to prevent memory leaks in SYSTEM_CONFIG_CACHE

This fixes the 404 error when committing navigation changes (communityId vs community.type mismatch)
and improves memory management by cleaning up expired cache entries.
- Update NavigationItem to highlight parent nav item when on tabs (e.g., /home/Settings highlights /home)
- Change default community ID fallback from console.error to console.warn
- Match highlighting logic between NavigationItem and NavigationEditor
- Add handleDeleteItem wrapper that checks if current pathname matches deleted item
- If on deleted item, find closest nav item (previous, then next, then first)
- Navigate to closest item after deletion to prevent 404 errors
- Import NavigationItem type from systemConfig
- Add tab config timestamp checking in hasUncommittedChanges to detect space customizations
- Always show X (cancel) and Save buttons in navigation edit mode
- Refactor navigation config API to ensure atomic database updates
- Compare local vs remote tab timestamps to detect uncommitted space changes
… cache

- Add MAX_ICON_CACHE constant (50) to cap cache size
- Evict oldest entries when cache exceeds max size using Map.keys().next().value
- Clear icon cache on navigation/route changes via useEffect watching pathname
- Prevents unbounded memory growth from icon wrapper cache
- Fix space tab 400 errors: Check for local tabs before fetching from storage to prevent errors for uncommitted navigation item tabs
- Prevent config warning spam: Add domain warning cache to only warn once per domain per session
- Add GlobalErrorHandler: Filter out known browser extension errors (PluginFactory, installHook) to reduce console noise
- Remove planning/review documentation files for production readiness
- Guard all console.log statements with NODE_ENV checks
- Debug logs now only appear in development mode
- Production builds will not include debug logging
- Preserves console.error and console.warn for error tracking
- Enhance type guard to validate each navigation item structure
  - Validates that items array elements have required string fields (id, label, href)
  - Prevents runtime errors from malformed navigation items
- Add timestamp freshness validation to prevent replay attacks
  - Rejects timestamps older than 15 minutes
  - Allows up to 5 minutes clock skew tolerance
  - Validates timestamp format before processing
- Replace broad tab existence check with precise change detection
  - Only commit spaces with actual uncommitted changes (renamed tabs, deleted tabs, new tabs, or modified tabs)
  - Prevents unnecessary commits for spaces that just have tabs but no modifications
  - Mirrors the logic used in hasUncommittedChanges for consistency

- Prevent render loop in Navigation component sidebar expansion
  - Use ref to track forced expansion per edit session
  - Only force sidebar expansion once when entering edit mode
  - Clear ref when exiting edit mode to prepare for next session
  - Prevents infinite loops if external code flips shrunk back to true
- Remove merge conflict markers left over from rebase
- Add missing useUIColors import
- Export NavIconBadge from NavigationItem.tsx for use in Navigation.tsx
- Fix type names to match exported interfaces (NavigationItemProps, NavigationButtonProps)
- Consolidate duplicate imports
- Remove unused 'ui' variable from systemConfig destructuring
- Remove duplicate local NavItem component definition (using imported NavItemComponent instead)
- Remove duplicate local NavButton component definition (using imported NavigationButton instead)
- These were leftover from rebase when components were moved to separate files
- Remove NavigationItemProps, NavigationButtonProps, and NavIconBadge from imports
- These were only needed for the duplicate local component definitions that were removed
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: 6

Caution

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

⚠️ Outside diff range comments (1)
src/common/components/organisms/Navigation.tsx (1)

405-457: Components defined inside render cause remounts.

NavItem is defined inside the Navigation component, meaning it's recreated on every render. This causes React to treat it as a new component type, unmounting and remounting the DOM elements. Move this outside the component or memoize it.

🔧 Suggested approach

Move NavItem outside the Navigation component:

// Define outside Navigation component
const NavItem: React.FC<NavigationItemProps & { 
  shrunk: boolean; 
  navTextStyle: React.CSSProperties;
  pathname: string | null;
  router: ReturnType<typeof useRouter>;
  onNavigate?: () => void;
}> = React.memo(({ ... }) => {
  // implementation
});

Or use the already-imported NavItemComponent from ./navigation/NavigationItem which appears to be the extracted version of this pattern.

🤖 Fix all issues with AI agents
In `@src/common/components/organisms/Navigation.tsx`:
- Around line 387-390: The useEffect that clears iconWrapperCacheRef.current on
every pathname change is too aggressive; remove the entire useEffect block (the
one referencing useEffect(() => { iconWrapperCacheRef.current.clear(); },
[pathname])) so icons are not remounted on every navigation and rely on the
existing LRU eviction logic (MAX_ICON_CACHE and the eviction code around the
icon cache) to bound memory; if you prefer targeted cleanup instead, replace the
effect with logic that only removes stale entries from iconWrapperCacheRef based
on age rather than clearing the whole map.
- Around line 708-718: The NavItemComponent is constructing
href={`/s/${username}`} without guarding for a missing username (in
Navigation.tsx), which can produce /s/undefined; update the render logic around
NavItemComponent so when username is falsy you either disable the link (pass
href={undefined} or a disabled prop) or conditionally render a non-link variant,
and ensure trackAnalyticsEvent(AnalyticsEvent.CLICK_MY_SPACE) is only called
when a valid username exists; target the NavItemComponent invocation and the
username variable to implement this guard.

In `@src/common/components/organisms/navigation/NavigationEditor.tsx`:
- Around line 153-168: The catch block in NavigationEditor (around the icon
update logic referencing itemId and iconName) currently re-throws the caught
Error after logging and showing toast.error, which defeats the local graceful
handling; remove the "throw error" path so the handler only logs the error and
shows the toast (and optionally return/exit the function after the toast) to
prevent propagation to the error boundary while preserving the console.error and
toast.error behavior.
- Around line 470-479: The NavItemComponent is building an href with a possibly
undefined username which can produce "/s/undefined"; update the NavigationEditor
rendering logic so you only pass a valid href when username exists (e.g.,
conditionally build href using username or fallback to a safe route/empty
string) or render the NavItemComponent without the href and/or with disable=true
when username is missing; locate the NavItemComponent usage in
NavigationEditor.tsx (the block that sets href={`/s/${username}`}) and change it
to guard the href creation against undefined username and ensure
analytics/onClick behavior remains correct.

In `@src/common/components/organisms/NavigationManagement.tsx`:
- Around line 105-113: In NavigationManagement.tsx update the catch block in the
commit function to surface real error details instead of masking 404s: log
error.response?.status and error.response?.data (or error.message) in the
console and include them in the toast/error message, only treat 404 as a
local-only fallback behind a clear dev flag or by checking NODE_ENV, and ensure
onCommit is called only on true success or an explicit dev-fallback path;
reference the existing catch(error: any) block, onCommit(), and
toast.success/toast.error calls when making these changes.
- Around line 58-63: The debouncedUpdateOrder created with useCallback needs
cleanup to avoid stale invocations on unmount; replace useCallback with useMemo
to create debouncedUpdateOrder = useMemo(() => debounce((newOrder)=>
updateNavigationOrder(newOrder), 300), [updateNavigationOrder]) and add an
effect that returns a cleanup which calls debouncedUpdateOrder.cancel() (or
cancels the debounced function) to run on unmount so pending debounced calls are
cleared.
♻️ Duplicate comments (5)
docs/README.md (1)

36-42: Documentation structure issue remains: "Customization" entry is still misplaced.

This issue was previously identified but has not been addressed. The "Customization" link (line 41) belongs under the "Themes" section but now appears under the "Navigation" heading. Please move it to be listed under "Themes" (after line 37) to maintain proper documentation organization.

📝 Suggested fix
 ### Themes
 - [Theme System](SYSTEMS/THEMES/OVERVIEW.md) - Theme architecture
+- [Customization](SYSTEMS/THEMES/CUSTOMIZATION.md) - Theme customization
 
 ### Navigation
 - [Navigation System](SYSTEMS/NAVIGATION/OVERVIEW.md) - Navigation editor and management
-- [Customization](SYSTEMS/THEMES/CUSTOMIZATION.md) - Theme customization
src/common/components/organisms/navigation/NavigationItem.tsx (1)

91-102: Redundant e.preventDefault() and unsafe type cast remain.

Line 96 calls e.preventDefault() redundantly (already called on line 93). The type cast on line 98 (as unknown as React.MouseEvent) is unsafe because keyboard events lack mouse-specific properties like button that handleClick checks on line 62.

🔧 Suggested fix
   const handleKeyDown = useCallback(
     (e: React.KeyboardEvent<HTMLAnchorElement>) => {
       if (disable) {
         e.preventDefault();
         return;
       }

       // Enter or Space key activates the link
       if (e.key === "Enter" || e.key === " ") {
         e.preventDefault();
-        if (e.key === " ") {
-          // Prevent page scroll on Space
-          e.preventDefault();
-        }
-        handleClick(e as unknown as React.MouseEvent<HTMLAnchorElement>);
+        // Navigate directly instead of casting to MouseEvent
+        if (!openInNewTab && href && href.startsWith("/")) {
+          router.push(href);
+          React.startTransition(() => {
+            onClick?.();
+            onNavigate?.();
+          });
+        } else {
+          onClick?.();
+          onNavigate?.();
+        }
       }
     },
-    [disable, handleClick]
+    [disable, href, openInNewTab, router, onClick, onNavigate]
   );
src/common/data/stores/app/navigation/navigationStore.ts (3)

369-377: Icon comparison uses reference equality, causing false positives.

Line 375 compares localItem.icon !== remoteItem.icon using reference equality. If icon is an object (e.g., { type: 'react-icon', name: 'FaHome' }), this will always return true even for identical values, causing phantom "uncommitted changes" detection.

🔧 Use deep comparison for icon
+import { isEqual } from "lodash";

 // In hasUncommittedChanges:
 return !remoteItem || 
   localItem.id !== remoteItem.id ||
   localItem.label !== remoteItem.label ||
   localItem.href !== remoteItem.href ||
-  localItem.icon !== remoteItem.icon ||
+  !isEqual(localItem.icon, remoteItem.icon) ||
   localItem.spaceId !== remoteItem.spaceId;

807-815: Navigation config PUT request lacks response validation.

The axiosBackend.put call at line 812 doesn't validate the response data. If the backend returns a 2xx status with an error payload in the body (e.g., { result: "error", error: { message: "..." } }), the update will be treated as successful and state will be updated incorrectly.

🔧 Add response validation
-await axiosBackend.put("/api/navigation/config", signedRequest);
+const response = await axiosBackend.put("/api/navigation/config", signedRequest);
+if (response.data?.result === "error") {
+  throw new Error(response.data.error?.message || "Navigation config update failed");
+}

831-845: Error logging exposes sensitive data and may fail on circular references.

Two issues:

  1. Line 839 logs error.response.headers which could include authentication tokens
  2. Line 844 uses JSON.stringify(error, null, 2) which throws on circular references (common in Axios errors)
🔧 Safer error logging
     if (error?.response) {
       console.error("API error response:", {
         status: error.response.status,
         statusText: error.response.statusText,
         data: error.response.data,
-        headers: error.response.headers,
       });
     } else if (error?.message) {
       console.error("Error message:", error.message);
     }
-    console.error("Full error object:", JSON.stringify(error, null, 2));
+    try {
+      console.error("Error details:", {
+        name: error?.name,
+        message: error?.message,
+        stack: error?.stack,
+      });
+    } catch {
+      console.error("Error (could not serialize):", error);
+    }
🧹 Nitpick comments (14)
src/common/components/organisms/navigation/ErrorBoundary.tsx (2)

6-10: Consider exposing reset capability to custom fallbacks.

When a custom fallback is provided (lines 72-74), the reset functionality is not accessible, meaning the custom fallback cannot recover from the error state without a full page refresh.

Consider using a render prop pattern or passing reset as a prop to the fallback:

♻️ Optional: Render prop pattern for custom fallback
 interface ErrorBoundaryProps {
   children: ReactNode;
-  fallback?: ReactNode;
+  fallback?: ReactNode | ((props: { error: Error; reset: () => void }) => ReactNode);
   onError?: (error: Error, errorInfo: ErrorInfo) => void;
 }

Then in render:

       // Use custom fallback if provided
       if (this.props.fallback) {
+        if (typeof this.props.fallback === 'function') {
+          return this.props.fallback({ error: this.state.error!, reset: this.handleReset });
+        }
         return this.props.fallback;
       }

Also applies to: 69-74


57-58: Reminder: Address TODO for error logging integration.

The TODO comment indicates plans to integrate with an error logging service (Sentry, LogRocket, etc.). This would be valuable for capturing navigation errors in production.

Would you like me to help implement the error logging integration or open an issue to track this task?

src/common/components/organisms/Sidebar.tsx (1)

76-81: Logic is correct; consider translating comment to English.

The conditional logic correctly shows the sidebar during navigation edit mode while hiding it during regular page edit mode. However, the comment on lines 76-77 appears to be in Portuguese ("Retornando às classes originais..."). For codebase consistency, consider translating to English.

♻️ Suggested comment translation
-  // Retornando às classes originais para preservar funcionalidade completa
-  // Isso mantém o comportamento de expansão/contração da barra lateral
   // Hide sidebar only for page edit mode, not navigation edit mode

The English comment on line 78-79 already captures the intent, so lines 76-77 could simply be removed.

tests/navigation/errorHandling.test.ts (1)

34-59: Consider using vi.spyOn for more idiomatic mocking.

The manual override of axios.isAxiosError works but could be cleaner using vitest's built-in mocking:

🔧 Suggested improvement
+import { describe, it, expect, vi } from 'vitest';
 import axios from 'axios';
 // ...

 it('should normalize network errors from axios', () => {
   const axiosError = Object.assign(new Error('Request failed'), {
     isAxiosError: true,
     response: {
       status: 500,
       // ...
     },
   }) as axios.AxiosError;
   
-  const originalIsAxiosError = axios.isAxiosError;
-  (axios as any).isAxiosError = (error: any) => error === axiosError || originalIsAxiosError(error);
+  const spy = vi.spyOn(axios, 'isAxiosError').mockImplementation(
+    (error: any) => error === axiosError
+  );
   
   try {
     const normalized = normalizeNavigationError(axiosError);
     expect(normalized.type).toBe('NETWORK');
     expect(normalized.retryable).toBe(true);
   } finally {
-    (axios as any).isAxiosError = originalIsAxiosError;
+    spy.mockRestore();
   }
 });
src/common/data/stores/app/space/spaceStore.ts (1)

739-767: Duplicate HTML detection logic.

The HTML error page detection (textContent.trim().startsWith("<!DOCTYPE")) appears twice: once in the catch block (lines 743-751) and again after the try block (lines 759-767). The second check on lines 759-767 handles cases where the request succeeds but returns an HTML error page (some servers return 200 with HTML error content).

Consider extracting this to a helper function to reduce duplication:

🔧 Suggested helper
const isHtmlErrorPage = (content: string): boolean => {
  const trimmed = content.trim();
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html");
};
src/app/page.tsx (1)

10-24: Simplified redirect logic looks good.

The redirect now uses the first configured navigation item's href, with a sensible /home fallback. This aligns with the PR's goal of using navigation config as the source of truth.

Minor simplification possible:

🔧 Simplified href handling
   if (firstNavItem?.href) {
-    // Remove leading slash if present and redirect
-    const href = firstNavItem.href.startsWith('/') 
-      ? firstNavItem.href 
-      : `/${firstNavItem.href}`;
+    // Ensure leading slash and redirect
+    const href = firstNavItem.href.startsWith('/') 
+      ? firstNavItem.href 
+      : `/${firstNavItem.href}`;
     redirect(href);
   }

Actually, the current code is correct. The comment says "Remove leading slash" but the code actually ensures one exists. Consider updating the comment:

-    // Remove leading slash if present and redirect
+    // Ensure href has leading slash for redirect
src/common/components/atoms/editable-text.tsx (1)

72-76: Double-click preventDefault may interfere with text selection.

The preventDefault() on double-click prevents the browser's native double-click-to-select-word behavior. While this prevents navigation, users may expect double-clicking to select text in the input. Consider removing e.preventDefault() if text selection is desired.

Optional: Allow native text selection
       onDoubleClick={(e) => {
         // Prevent navigation when double-clicking on input (select all text)
         e.stopPropagation();
-        e.preventDefault();
       }}
src/common/components/organisms/navigation/useNavigation.ts (1)

60-70: Unused store selectors: deleteNavigationItem and renameNavigationItem.

Lines 65-66 destructure deleteNavigationItem and renameNavigationItem from the store, but these are never used within this hook. This creates unnecessary re-renders when these functions change.

🧹 Remove unused selectors
   const {
     loadNavigation,
     localNavigation,
     hasUncommittedChanges,
     createNavigationItem,
-    deleteNavigationItem,
-    renameNavigationItem,
     updateNavigationOrder,
     commitNavigationChanges,
     resetNavigationChanges,
   } = useAppStore((state) => ({
     loadNavigation: state.navigation.loadNavigation,
     localNavigation: state.navigation.localNavigation,
     hasUncommittedChanges: state.navigation.hasUncommittedChanges,
     createNavigationItem: state.navigation.createNavigationItem,
-    deleteNavigationItem: state.navigation.deleteNavigationItem,
-    renameNavigationItem: state.navigation.renameNavigationItem,
     updateNavigationOrder: state.navigation.updateNavigationOrder,
     commitNavigationChanges: state.navigation.commitNavigationChanges,
     resetNavigationChanges: state.navigation.resetNavigationChanges,
   }));
src/common/providers/GlobalErrorHandler.tsx (1)

34-48: Consider defensive handling for toString() calls.

The arg?.toString check at line 40 could throw on certain exotic objects (e.g., revoked Proxies). While rare, wrapping this in a try-catch would make the error handler more robust.

🛠️ Suggested improvement
     const isBrowserExtensionError = (args: any[]): boolean => {
       const errorString = args
         .map((arg) => {
-          if (typeof arg === "string") return arg;
-          if (arg?.message) return arg.message;
-          if (arg?.stack) return arg.stack;
-          if (arg?.toString) return arg.toString();
-          return String(arg);
+          try {
+            if (typeof arg === "string") return arg;
+            if (arg?.message) return arg.message;
+            if (arg?.stack) return arg.stack;
+            if (arg?.toString) return arg.toString();
+            return String(arg);
+          } catch {
+            return "";
+          }
         })
         .join(" ");
src/common/components/organisms/NavigationManagement.tsx (1)

54-56: Simplify the items selection logic.

The nested ternary is hard to follow. Consider restructuring for clarity.

♻️ Suggested refactor
-  // Use local navigation items if available, otherwise fall back to props
-  // Only use localNavigation if it has items (not empty array from initial state)
-  const items = localNavigation.length > 0 || navigationItems.length === 0
-    ? localNavigation.length > 0 ? localNavigation : navigationItems
-    : navigationItems;
+  // Use local navigation items if available, otherwise fall back to props
+  const items = useMemo(() => {
+    if (localNavigation.length > 0) {
+      return localNavigation;
+    }
+    return navigationItems;
+  }, [localNavigation, navigationItems]);
src/common/components/organisms/navigation/NavigationEditor.tsx (2)

103-111: iconButtonRefs grows unboundedly.

The refs map accumulates entries for every item but is never cleaned up when items are deleted. Over time with many item additions/deletions, this could leak memory.

🧹 Consider cleanup on item removal
+  // Clean up refs for deleted items
+  useEffect(() => {
+    const currentIds = new Set(items.map(item => item.id));
+    Object.keys(iconButtonRefs.current).forEach(id => {
+      if (!currentIds.has(id)) {
+        delete iconButtonRefs.current[id];
+      }
+    });
+  }, [items]);

382-416: Duplicate delete logic between onClick and onKeyDown.

The delete handler logic is duplicated. Extract to a shared function.

♻️ Suggested refactor
+  const handleDeleteClick = useCallback((item: NavigationItem, e: React.SyntheticEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    if (process.env.NODE_ENV === 'development') {
+      console.log('[NavigationEditor] Deleting navigation item:', {
+        itemId: item.id,
+        label: item.label,
+        href: item.href
+      });
+    }
+    onDelete(item.id);
+    toast.success("Navigation item deleted");
+  }, [onDelete]);

   // Then use in both handlers:
   <button
-    onClick={(e) => {
-      e.preventDefault();
-      e.stopPropagation();
-      // ... duplicated logic
-    }}
-    onKeyDown={(e) => {
-      if (e.key === "Enter" || e.key === " ") {
-        e.preventDefault();
-        e.stopPropagation();
-        // ... duplicated logic
-      }
-    }}
+    onClick={(e) => handleDeleteClick(item, e)}
+    onKeyDown={(e) => {
+      if (e.key === "Enter" || e.key === " ") {
+        handleDeleteClick(item, e);
+      }
+    }}
src/common/components/organisms/Navigation.tsx (2)

313-324: createUrlIconWrapper creates new component on each call.

Despite being wrapped in useCallback, this function returns a new React.memo component instance each time it's called. This defeats the memoization and causes remounts. The caching in iconFor mitigates this, but the function itself should return stable references.

♻️ Consider pre-creating or lazily caching the wrapper

The current pattern works because iconFor caches the result, but createUrlIconWrapper is misleadingly named as a factory when it should ideally be integrated into the caching logic directly. The useCallback on it provides no benefit since each call creates a new component anyway.

-  // Reusable component factory for URL icons
-  const createUrlIconWrapper = useCallback((src: string): React.FC => {
-    const CustomIconWrapper = React.memo(() => (
+  // Factory for URL icon components (caching handled by iconFor)
+  const createUrlIconWrapper = (src: string): React.FC => {
+    const CustomIconWrapper: React.FC = () => (
       <img 
         src={src} 
         alt="icon" 
         className="w-5 h-5 rounded object-contain" 
         aria-hidden="true"
       />
-    ));
+    );
     CustomIconWrapper.displayName = `CustomIcon(${src})`;
     return CustomIconWrapper;
-  }, []);
+  };

The React.memo wrapper is unnecessary since iconFor already caches the component.


641-641: Redundant conditional expression.

username ? username : undefined is equivalent to just username since username is already string | undefined.

-                  username={username ? username : undefined}
+                  username={username}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f49f64 and d5865cf.

📒 Files selected for processing (35)
  • docs/DOCUMENTATION_OVERVIEW.md
  • docs/GETTING_STARTED.md
  • docs/README.md
  • docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md
  • docs/SYSTEMS/NAVIGATION/OVERVIEW.md
  • scripts/seed.ts
  • src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx
  • src/app/[navSlug]/[[...tabName]]/page.tsx
  • src/app/page.tsx
  • src/common/components/atoms/editable-text.tsx
  • src/common/components/atoms/reorderable-tab.tsx
  • src/common/components/organisms/Navigation.tsx
  • src/common/components/organisms/NavigationManagement.tsx
  • src/common/components/organisms/Sidebar.tsx
  • src/common/components/organisms/TabBar.tsx
  • src/common/components/organisms/navigation/ErrorBoundary.tsx
  • src/common/components/organisms/navigation/NavigationEditor.tsx
  • src/common/components/organisms/navigation/NavigationItem.tsx
  • src/common/components/organisms/navigation/constants.ts
  • src/common/components/organisms/navigation/errorHandling.ts
  • src/common/components/organisms/navigation/useNavigation.ts
  • src/common/data/stores/app/index.tsx
  • src/common/data/stores/app/navigation/navigationStore.ts
  • src/common/data/stores/app/space/spaceStore.ts
  • src/common/lib/theme/ThemeSettingsEditor.tsx
  • src/common/providers/GlobalErrorHandler.tsx
  • src/common/providers/index.tsx
  • src/common/utils/layoutFormatUtils.ts
  • src/common/utils/navUtils.ts
  • src/common/utils/tabUtils.ts
  • src/config/index.ts
  • src/config/loaders/registry.ts
  • src/config/systemConfig.ts
  • src/pages/api/navigation/config.ts
  • tests/navigation/errorHandling.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/common/utils/tabUtils.ts
  • src/config/systemConfig.ts
  • src/app/[navSlug]/[[...tabName]]/page.tsx
  • src/config/index.ts
  • docs/GETTING_STARTED.md
  • src/common/components/organisms/navigation/errorHandling.ts
  • src/common/lib/theme/ThemeSettingsEditor.tsx
  • src/common/utils/layoutFormatUtils.ts
  • docs/SYSTEMS/NAVIGATION/OVERVIEW.md
  • src/common/data/stores/app/index.tsx
  • src/common/utils/navUtils.ts
  • src/common/components/atoms/reorderable-tab.tsx
  • docs/DOCUMENTATION_OVERVIEW.md
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-29T18:25:31.969Z
Learnt from: j-paterson
Repo: blankdotspace/space-system PR: 1625
File: src/common/lib/utils/tokenGates.ts:12-20
Timestamp: 2025-12-29T18:25:31.969Z
Learning: Gate tokens in src/common/lib/utils/tokenGates.ts are currently fetched redundantly from 3 client components (CreateCast.tsx, BackgroundGenerator.tsx, NogsGateButton.tsx) with no caching, and should be refactored to use a cached API route with a shared client hook to reduce redundant server calls.

Applied to files:

  • src/common/components/organisms/Navigation.tsx
📚 Learning: 2025-12-31T21:13:36.931Z
Learnt from: willyogo
Repo: blankdotspace/space-system PR: 1641
File: src/fidgets/farcaster/components/CastRow.tsx:247-250
Timestamp: 2025-12-31T21:13:36.931Z
Learning: In src/fidgets/farcaster/components/CastRow.tsx (and similar UI components), hardcoded semi-transparent colors like `rgba(128, 128, 128, 0.2)` for borders and backgrounds are acceptable because they adapt to any background color, eliminating the need for theme tokens in ui_config for these specific use cases.

Applied to files:

  • src/common/components/organisms/Navigation.tsx
🧬 Code graph analysis (11)
src/common/components/atoms/editable-text.tsx (1)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/common/data/stores/app/space/spaceStore.ts (3)
src/common/lib/signedFiles.ts (1)
  • SignedFile (16-18)
src/pages/api/space/registry/[spaceId]/index.ts (1)
  • UpdateTabOrderRequest (30-30)
src/pages/api/space/homebase/tabOrder.ts (1)
  • UpdateTabOrderRequest (15-15)
src/common/components/organisms/TabBar.tsx (1)
src/common/utils/tabUtils.ts (1)
  • generateUniqueName (44-58)
tests/navigation/errorHandling.test.ts (1)
src/common/components/organisms/navigation/errorHandling.ts (3)
  • normalizeNavigationError (18-48)
  • NavigationError (6-10)
  • getUserFriendlyMessage (56-69)
src/common/providers/index.tsx (1)
src/common/providers/GlobalErrorHandler.tsx (1)
  • GlobalErrorHandler (13-78)
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (3)
src/common/types/spaceData.ts (2)
  • NavPageSpacePageData (77-83)
  • SPACE_TYPES (7-13)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/app/(spaces)/PublicSpace.tsx (1)
  • PublicSpace (33-463)
src/config/loaders/registry.ts (1)
src/config/systemConfig.ts (1)
  • SystemConfig (27-37)
src/common/components/organisms/navigation/NavigationItem.tsx (5)
src/config/index.ts (1)
  • SystemConfig (106-106)
src/config/systemConfig.ts (2)
  • SystemConfig (27-37)
  • NavigationItem (156-164)
src/common/lib/hooks/useUIColors.ts (1)
  • useUIColors (15-49)
src/common/components/atoms/badge.tsx (1)
  • Badge (40-40)
src/common/lib/utils/mergeClasses.ts (1)
  • mergeClasses (4-6)
src/common/components/organisms/Navigation.tsx (9)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (55-57)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/common/lib/hooks/useUIColors.ts (1)
  • useUIColors (15-49)
src/common/components/organisms/navigation/NavigationItem.tsx (2)
  • NavigationItemProps (27-38)
  • NavigationItem (146-160)
src/common/data/stores/app/index.tsx (1)
  • useAppStore (174-174)
src/config/systemConfig.ts (1)
  • NavigationItem (156-164)
src/common/components/organisms/navigation/ErrorBoundary.tsx (1)
  • NavigationErrorBoundary (22-113)
src/common/components/organisms/navigation/NavigationEditor.tsx (1)
  • NavigationEditor (508-508)
src/common/lib/utils/analyticsUtils.ts (1)
  • trackAnalyticsEvent (5-12)
src/pages/api/navigation/config.ts (7)
src/config/systemConfig.ts (1)
  • NavigationConfig (144-149)
src/common/lib/signedFiles.ts (3)
  • Signable (20-23)
  • isSignable (29-41)
  • validateSignable (43-52)
src/common/data/api/requestHandler.ts (2)
  • NounspaceResponse (9-16)
  • requestHandler (31-51)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/utils/navUtils.ts (2)
  • validateNavItemLabel (19-35)
  • validateNavItemHref (42-72)
src/supabase/database.d.ts (1)
  • Json (3-9)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/common/data/stores/app/navigation/navigationStore.ts (7)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-13)
src/config/systemConfig.ts (2)
  • NavigationItem (156-164)
  • NavigationConfig (144-149)
src/common/data/stores/createStore.ts (2)
  • StoreSet (51-51)
  • StoreGet (52-52)
src/common/data/stores/app/index.tsx (1)
  • AppStore (43-56)
src/common/utils/navUtils.ts (5)
  • isDuplicateNavLabel (82-90)
  • generateUniqueNavLabel (164-179)
  • validateNavItemHref (42-72)
  • isDuplicateNavHref (99-108)
  • generateUniqueHrefFromLabel (118-154)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/pages/api/space/registry/index.ts (1)
  • RegisterNewSpaceResponse (114-114)
🔇 Additional comments (39)
src/common/components/organisms/TabBar.tsx (2)

16-16: LGTM!

Clean import consolidation - both validateTabName and generateUniqueName are appropriately imported from the same utility module.


198-206: Good refactor to centralized utility.

The delegation to generateUniqueName correctly centralizes the deduplication logic. The null return (when max iterations exceeded) is properly handled by the caller at line 169.

scripts/seed.ts (1)

20-20: LGTM - Environment variable rename is consistently applied.

The rename from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY is consistently updated across the comment (line 20), usage (line 64), and error message (line 70) in this file.

Also applies to: 64-64, 70-70

src/common/components/organisms/navigation/ErrorBoundary.tsx (1)

35-59: Error handling lifecycle is correctly implemented.

The separation between getDerivedStateFromError (for synchronous state update during render) and componentDidCatch (for side effects like logging and storing errorInfo) follows React's recommended error boundary pattern.

src/common/components/organisms/navigation/constants.ts (1)

1-33: Well-documented constants with sensible values.

The constants are appropriately named, reasonably valued, and well-documented. The inline JSDoc comments clearly explain the purpose and rationale for each constant, which aids maintainability.

src/common/components/organisms/Sidebar.tsx (1)

37-48: Memoization is correctly implemented.

The useMemo dependencies correctly include both editMode and navEditMode, ensuring the context value updates when either edit mode changes. The state setters (setEditMode, etc.) are stable and don't need to be in the dependency array.

src/config/loaders/registry.ts (3)

83-95: Correctly propagates communityId to SystemConfig.

The transformRowToSystemConfig function now correctly accepts and includes communityId in the returned SystemConfig, aligning with the updated interface definition.


97-140: Cache cleanup strategy is reasonable.

The dual approach—lazy deletion on read (line 121) plus batch cleanup on write when cache exceeds 10 entries—effectively prevents unbounded memory growth while minimizing cleanup overhead. The 60-second TTL combined with read-time eviction ensures stale entries don't persist long.


188-258: Database loading with proper validation and caching.

The loadCommunityConfigFromDatabase function correctly:

  • Checks cache before querying
  • Validates required fields and throws descriptive errors for data integrity issues
  • Distinguishes between "not found" (returns null) and transient errors (logs and returns null)
  • Caches successful results

The updated call to transformRowToSystemConfig(data, communityId) on line 252 correctly passes the communityId parameter.

tests/navigation/errorHandling.test.ts (2)

1-7: LGTM! Previous concern addressed.

The test file now correctly imports NavigationError, normalizeNavigationError, and getUserFriendlyMessage from the production module, ensuring tests validate the actual implementation rather than a local copy.


103-151: Good test coverage for user-friendly messages.

The tests comprehensively cover all NavigationError variants including the retryable vs non-retryable network error distinction.

src/common/components/organisms/navigation/NavigationItem.tsx (2)

146-160: Memo comparison omits onClick and onNavigate - verify these are stable.

The custom comparison function doesn't include onClick, onNavigate, or Icon in the equality check. If these callbacks change without other props changing, the component won't re-render and will use stale closures.

This is fine if callers always wrap these in useCallback, but could cause subtle bugs otherwise.


111-134: Good accessibility implementation.

The component properly handles:

  • aria-current="page" for active state
  • aria-label when shrunk
  • tabIndex={-1} when disabled
  • role="none" on <li> for proper menu semantics
src/common/data/stores/app/space/spaceStore.ts (4)

260-291: Good defensive initialization of remoteSpaces.

The initialization guard at lines 267-275 prevents errors when committing tabs for newly created spaces that don't yet have a remoteSpaces entry. The additional check on line 281 prevents attempting to delete non-existent tabs.


607-617: Proactive initialization prevents commit failures for new spaces.

Pre-initializing remoteSpaces[spaceId] before the batch operations ensures that commitSpaceTabToDatabase and other operations have a valid remote space entry to work with, even for newly created spaces.


680-702: Smart optimization for uncommitted tabs.

Skipping storage fetch when a tab exists locally but not remotely prevents unnecessary 400/404 errors for new uncommitted tabs. This improves UX during navigation editing.


873-884: Graceful handling of missing tabOrder file.

This correctly handles new spaces that haven't committed their tab order yet, falling back to local order without throwing errors.

src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (2)

72-126: Local-first resolution logic looks correct.

The data resolution properly:

  1. Handles rename transitions via hrefMappings
  2. Searches both current and mapped hrefs
  3. Falls back to server data when local data unavailable
  4. Returns null for proper 404 handling

One consideration: if localNavItem.spaceId exists but spaceLocalSpaces[localNavItem.spaceId] doesn't, the code falls through to server data. This seems intentional but could potentially cause a brief flash if local nav exists but space hasn't been initialized yet.


129-160: Correct hook ordering with 404 handling.

The code properly calls all hooks before the conditional return, satisfying React's Rules of Hooks. The comment on line 129 documents this intentional pattern clearly.

src/app/page.tsx (1)

7-26: Clean root redirect implementation.

The simplified redirect logic correctly:

  1. Loads system config for navigation items
  2. Redirects to the first nav item's href
  3. Falls back to /home if no items configured

This is a good simplification from the previous implementation that loaded tab order from storage.

src/common/components/atoms/editable-text.tsx (2)

28-33: LGTM! Good state synchronization pattern.

The useEffect correctly syncs internal state with props only when not actively editing, preventing loss of in-progress edits.


35-52: LGTM! Validation flow is well-structured.

The validation logic correctly:

  • Trims input before validation
  • Falls back to validateTabName when no custom validator is provided
  • Resets to original value on validation failure while keeping edit mode active
src/common/data/stores/app/navigation/navigationStore.ts (3)

159-185: LGTM! Atomic state update combining space and navigation item creation.

The single set call atomically updates both localSpaces and localNavigation, preventing inconsistent state if one operation fails. This addresses a previous review concern.


210-226: LGTM! Proper cleanup of associated local space on deletion.

The deletion logic now correctly removes both the navigation item and its associated local space in a single atomic operation, preventing memory leaks.


616-629: LGTM! Space registration failures now abort the entire commit.

This change properly fails the commit when space registration fails, preventing navigation items from pointing to non-existent spaces. This addresses a critical previous review concern about data consistency.

src/common/components/organisms/navigation/useNavigation.ts (3)

130-151: LGTM! Safe cancel navigation pattern prevents 404 flashes.

The pendingCancelTargetRef pattern correctly defers the navigation reset until the route change completes, preventing 404 errors when canceling from a locally-created (uncommitted) navigation item.


116-121: LGTM! Proper cleanup of debounced function.

The cleanup effect correctly cancels pending debounced calls on unmount, preventing memory leaks and state updates on unmounted components.


336-337: Remove this comment—the current implementation is correct.

hasUncommittedChanges is retrieved from the store as a function () => boolean and intentionally evaluated at line 336 to return a boolean value. This matches the hook's return type interface (hasUncommittedChanges: boolean) and consumer expectations. Consumers receive a boolean snapshot of the state, not a function. If reactive updates are needed, that would require consumers to subscribe directly to the store or the hook to return an observable, but the current design is intentional.

Likely an incorrect or invalid review comment.

src/pages/api/navigation/config.ts (5)

32-69: LGTM! Type guard now validates navigation item structure.

The strengthened type guard correctly validates that each item has required id, label, and href fields as strings, preventing runtime errors from malformed items.


81-103: LGTM! Timestamp freshness validation prevents replay attacks.

The isTimestampFresh function correctly:

  • Rejects timestamps older than 15 minutes
  • Allows 5 minutes of clock skew for future timestamps
  • Validates timestamp format before comparison

309-354: LGTM! Row-level update verification prevents silent failures.

The update now uses .select() to verify rows were updated and returns a 409 error if no rows matched. This handles race conditions where the config was unpublished between fetch and update.


375-424: Space cleanup is best-effort after successful config update.

The cleanup logic correctly:

  1. Only runs after the config update succeeds
  2. Logs warnings but doesn't fail the request if cleanup fails
  3. Handles both storage files and space registrations

This is appropriate since the primary operation (config update) succeeded and cleanup failures can be addressed separately.


159-170: Debug logging is appropriately gated to development.

The detailed logging of navigation items (including spaceId) is now properly gated behind process.env.NODE_ENV === 'development', addressing the previous concern about production logging.

docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md (3)

193-195: LGTM! SystemConfig documentation updated with new fields.

The documentation correctly reflects the addition of adminIdentityPublicKeys for admin authorization and communityId for API operations.


210-212: Helpful clarification distinguishing communityId from community.type.

This note prevents confusion between the database identifier (communityId) and the semantic community descriptor (community.type).


529-532: LGTM! Navigation editor files properly documented.

The related files section now includes all new navigation-related components: NavigationEditor, useNavigation hook, navigationStore, and the API endpoint.

src/common/providers/index.tsx (1)

43-68: LGTM!

The GlobalErrorHandler is correctly placed at the top level outside the provider chain since it only sets up console overrides and doesn't require any React context. The fragment wrapper is appropriate for adding this sibling component.

src/common/providers/GlobalErrorHandler.tsx (1)

71-74: LGTM!

Cleanup correctly restores the original console handlers on unmount, preventing memory leaks and ensuring proper handler restoration when the component is removed.

src/common/components/organisms/Navigation.tsx (1)

131-149: LGTM - Addresses past review feedback.

The forcedExpansionRef pattern correctly prevents render loops when forcing sidebar expansion during edit mode, and properly clears the ref when exiting edit mode.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +387 to +390
// Clear icon cache on navigation/route changes to prevent memory growth
useEffect(() => {
iconWrapperCacheRef.current.clear();
}, [pathname]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Icon cache cleared on every navigation.

Clearing the entire cache on every pathname change is overly aggressive. This defeats the purpose of caching and causes icons to remount on every navigation. Consider only clearing stale entries or removing this effect entirely since the LRU eviction at lines 356-376 already handles unbounded growth.

🔧 Suggested fix - remove aggressive clearing
-  // Clear icon cache on navigation/route changes to prevent memory growth
-  useEffect(() => {
-    iconWrapperCacheRef.current.clear();
-  }, [pathname]);

The LRU-style eviction (MAX_ICON_CACHE = 50) already prevents unbounded growth. Clearing on every navigation causes unnecessary component remounts.

📝 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
// Clear icon cache on navigation/route changes to prevent memory growth
useEffect(() => {
iconWrapperCacheRef.current.clear();
}, [pathname]);
🤖 Prompt for AI Agents
In `@src/common/components/organisms/Navigation.tsx` around lines 387 - 390, The
useEffect that clears iconWrapperCacheRef.current on every pathname change is
too aggressive; remove the entire useEffect block (the one referencing
useEffect(() => { iconWrapperCacheRef.current.clear(); }, [pathname])) so icons
are not remounted on every navigation and rely on the existing LRU eviction
logic (MAX_ICON_CACHE and the eviction code around the icon cache) to bound
memory; if you prefer targeted cleanup instead, replace the effect with logic
that only removes stale entries from iconWrapperCacheRef based on age rather
than clearing the whole map.

Comment thread src/common/components/organisms/Navigation.tsx
Comment on lines +153 to +168
} catch (error: unknown) {
const errorMessage = error instanceof Error
? error.message
: "Failed to update icon";
console.error('[NavigationEditor] Icon update failed:', {
itemId,
iconName,
error: errorMessage
});
toast.error(errorMessage);

// Re-throw for error boundaries
if (error instanceof Error) {
throw error;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Re-throwing after catch defeats error handling purpose.

Lines 165-167 re-throw the error after showing a toast, which will propagate to the error boundary and potentially crash the UI. If you want the error boundary to catch it, don't catch it here. If you want to show a toast and continue, don't re-throw.

🔧 Suggested fix - remove re-throw to allow graceful handling
     } catch (error: unknown) {
       const errorMessage = error instanceof Error 
         ? error.message 
         : "Failed to update icon";
       console.error('[NavigationEditor] Icon update failed:', {
         itemId,
         iconName,
         error: errorMessage
       });
       toast.error(errorMessage);
-      
-      // Re-throw for error boundaries
-      if (error instanceof Error) {
-        throw error;
-      }
     }
🤖 Prompt for AI Agents
In `@src/common/components/organisms/navigation/NavigationEditor.tsx` around lines
153 - 168, The catch block in NavigationEditor (around the icon update logic
referencing itemId and iconName) currently re-throws the caught Error after
logging and showing toast.error, which defeats the local graceful handling;
remove the "throw error" path so the handler only logs the error and shows the
toast (and optionally return/exit the function after the toast) to prevent
propagation to the error boundary while preserving the console.error and
toast.error behavior.

Comment on lines +470 to +479
{isLoggedIn && (
<NavItemComponent
label="My Space"
Icon={CurrentUserImage}
href={`/s/${username}`}
onClick={() => trackAnalyticsEvent(AnalyticsEvent.CLICK_MY_SPACE)}
disable={true}
shrunk={isShrunk}
systemConfig={systemConfig}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential /s/undefined URL when username is missing.

username is an optional prop, but line 474 uses it directly in the href. If username is undefined, this creates an invalid URL /s/undefined.

🔧 Suggested fix
         {isLoggedIn && (
           <NavItemComponent
             label="My Space"
             Icon={CurrentUserImage}
-            href={`/s/${username}`}
+            href={username ? `/s/${username}` : "#"}
             onClick={() => trackAnalyticsEvent(AnalyticsEvent.CLICK_MY_SPACE)}
-            disable={true}
+            disable={!username}
             shrunk={isShrunk}
             systemConfig={systemConfig}
           />
         )}
📝 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
{isLoggedIn && (
<NavItemComponent
label="My Space"
Icon={CurrentUserImage}
href={`/s/${username}`}
onClick={() => trackAnalyticsEvent(AnalyticsEvent.CLICK_MY_SPACE)}
disable={true}
shrunk={isShrunk}
systemConfig={systemConfig}
/>
{isLoggedIn && (
<NavItemComponent
label="My Space"
Icon={CurrentUserImage}
href={username ? `/s/${username}` : "#"}
onClick={() => trackAnalyticsEvent(AnalyticsEvent.CLICK_MY_SPACE)}
disable={!username}
shrunk={isShrunk}
systemConfig={systemConfig}
/>
)}
🤖 Prompt for AI Agents
In `@src/common/components/organisms/navigation/NavigationEditor.tsx` around lines
470 - 479, The NavItemComponent is building an href with a possibly undefined
username which can produce "/s/undefined"; update the NavigationEditor rendering
logic so you only pass a valid href when username exists (e.g., conditionally
build href using username or fallback to a safe route/empty string) or render
the NavItemComponent without the href and/or with disable=true when username is
missing; locate the NavItemComponent usage in NavigationEditor.tsx (the block
that sets href={`/s/${username}`}) and change it to guard the href creation
against undefined username and ensure analytics/onClick behavior remains
correct.

Comment on lines +58 to +63
const debouncedUpdateOrder = useCallback(
debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing cleanup for debounced function.

The debounced function should be cancelled on unmount to prevent stale updates. Use useMemo instead of useCallback and add cleanup.

🔧 Proposed fix
-  const debouncedUpdateOrder = useCallback(
-    debounce((newOrder: NavigationItem[]) => {
+  const debouncedUpdateOrder = useMemo(
+    () => debounce((newOrder: NavigationItem[]) => {
       updateNavigationOrder(newOrder);
     }, 300),
     [updateNavigationOrder]
   );
+
+  // Cleanup debounce on unmount
+  useEffect(() => {
+    return () => {
+      debouncedUpdateOrder.cancel();
+    };
+  }, [debouncedUpdateOrder]);
📝 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
const debouncedUpdateOrder = useCallback(
debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
const debouncedUpdateOrder = useMemo(
() => debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
// Cleanup debounce on unmount
useEffect(() => {
return () => {
debouncedUpdateOrder.cancel();
};
}, [debouncedUpdateOrder]);
🤖 Prompt for AI Agents
In `@src/common/components/organisms/NavigationManagement.tsx` around lines 58 -
63, The debouncedUpdateOrder created with useCallback needs cleanup to avoid
stale invocations on unmount; replace useCallback with useMemo to create
debouncedUpdateOrder = useMemo(() => debounce((newOrder)=>
updateNavigationOrder(newOrder), 300), [updateNavigationOrder]) and add an
effect that returns a cleanup which calls debouncedUpdateOrder.cancel() (or
cancels the debounced function) to run on unmount so pending debounced calls are
cleared.

Comment on lines +105 to +113
} catch (error: any) {
console.error("Failed to commit navigation changes:", error);
// For local testing, still show success if it's just a 404 (API not implemented)
if (error?.response?.status === 404) {
toast.success("Navigation changes staged locally (API endpoints not yet implemented)");
onCommit?.();
} else {
toast.error("Failed to commit navigation changes");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Masking 404 errors as success may hide real issues.

The PR comments report a 400 Bad Request causing commit failures. Treating 404 as a success (lines 108-110) while showing a generic error for other statuses may obscure the actual problem. The commit failure mentioned is a 400, not 404, so this code path wouldn't help with that issue.

Consider logging the actual error status and message to help diagnose failures:

🐛 Suggested improvement
     } catch (error: any) {
-      console.error("Failed to commit navigation changes:", error);
+      console.error("Failed to commit navigation changes:", {
+        status: error?.response?.status,
+        message: error?.message,
+        data: error?.response?.data,
+      });
       // For local testing, still show success if it's just a 404 (API not implemented)
       if (error?.response?.status === 404) {
         toast.success("Navigation changes staged locally (API endpoints not yet implemented)");
         onCommit?.();
       } else {
-        toast.error("Failed to commit navigation changes");
+        const errorMessage = error?.response?.data?.message || error?.message || "Failed to commit navigation changes";
+        toast.error(errorMessage);
       }
📝 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
} catch (error: any) {
console.error("Failed to commit navigation changes:", error);
// For local testing, still show success if it's just a 404 (API not implemented)
if (error?.response?.status === 404) {
toast.success("Navigation changes staged locally (API endpoints not yet implemented)");
onCommit?.();
} else {
toast.error("Failed to commit navigation changes");
}
} catch (error: any) {
console.error("Failed to commit navigation changes:", {
status: error?.response?.status,
message: error?.message,
data: error?.response?.data,
});
// For local testing, still show success if it's just a 404 (API not implemented)
if (error?.response?.status === 404) {
toast.success("Navigation changes staged locally (API endpoints not yet implemented)");
onCommit?.();
} else {
const errorMessage = error?.response?.data?.message || error?.message || "Failed to commit navigation changes";
toast.error(errorMessage);
}
🤖 Prompt for AI Agents
In `@src/common/components/organisms/NavigationManagement.tsx` around lines 105 -
113, In NavigationManagement.tsx update the catch block in the commit function
to surface real error details instead of masking 404s: log
error.response?.status and error.response?.data (or error.message) in the
console and include them in the toast/error message, only treat 404 as a
local-only fallback behind a clear dev flag or by checking NODE_ENV, and ensure
onCommit is called only on true success or an explicit dev-fallback path;
reference the existing catch(error: any) block, onCommit(), and
toast.success/toast.error calls when making these changes.

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.

2 participants