Skip to content

Conversation

@j-paterson
Copy link
Collaborator

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

Summary by CodeRabbit

  • New Features

    • Admins can now edit navigation items with drag-and-drop reordering, inline renaming, and item creation/deletion.
    • Added commit/cancel workflow for navigation edits with toast notifications.
  • Bug Fixes

    • Improved space data fallback handling when server data is unavailable.
    • Fixed click event propagation in editable fields to prevent unintended navigation.
  • Chores

    • Updated seed script dependencies.

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

…e patterns

Remove redundant state tracking (localSpaces, renamedItems, orderChanged) and make localNavigation the single source of truth for staged changes. Simplify change detection by directly comparing localNavigation vs remoteNavigation. Remove unused spaceName parameter, add proper TypeScript type for navPage registration, and replace manual tabOrder commit logic with commitAllSpaceChanges to follow existing spaceStore patterns and reduce code duplication.
@vercel
Copy link

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 Error Error Dec 19, 2025 11:38pm

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

coderabbitai bot commented Dec 19, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes introduce admin-level navigation editing capabilities with client-side staging, a local state store for navigation items, an API endpoint for persisting navigation configuration, and fallback logic to reconstruct navigation data client-side when server-provided data is unavailable. Additionally, event handling improvements are made to editable text inputs to prevent unintended navigation during editing.

Changes

Cohort / File(s) Summary
Configuration & API
scripts/seed.ts, src/pages/api/navigation/config.ts
Renames environment variable from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY. Introduces new API route with type guards and request handler to validate, authenticate, and persist navigation configuration updates with admin authorization checks.
Navigation Store
src/common/data/stores/app/index.tsx, src/common/data/stores/app/navigation/navigationStore.ts
Adds navigation store slice to AppStore. Introduces navigationStore module with local/remote item staging, CRUD operations (create, rename, delete, reorder), change tracking, and commit logic that syncs with API and manages space registration.
UI Components
src/common/components/atoms/editable-text.tsx, src/common/components/organisms/Navigation.tsx, src/common/components/organisms/NavigationManagement.tsx, src/common/components/organisms/Sidebar.tsx
Enhances editable-text input with click event propagation prevention. Implements admin navigation editing in Navigation with edit/normal mode toggle, draggable reordering, inline item editing, and delete/add controls. Introduces new NavigationManagement component for CRUD operations and debounced reordering. Adds navEditMode state to SidebarContextValue for edit mode management.
Page Components
src/app/[navSlug]/[[...tabName]]/page.tsx, src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx
Modifies server logic to pass adminIdentityPublicKeys to client when spaceData is missing. Updates NavPageSpace to reconstruct NavPageSpacePageData from local stores as fallback, handle null server data, and accept navSlug and adminIdentityPublicKeys props.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin User
    participant NavUI as Navigation UI
    participant Store as Local Store
    participant API as API Endpoint
    participant DB as Database

    rect rgb(200, 220, 240)
    Note over Admin,DB: Navigation Editing & Commit Flow
    Admin->>NavUI: Toggle Edit Mode
    Admin->>NavUI: Create/Rename/Delete/Reorder Items
    NavUI->>Store: Update local navigation state
    Store-->>NavUI: Reflect changes (uncommitted)
    Admin->>NavUI: Click Commit
    NavUI->>API: POST signed update request
    API->>DB: Validate admin, update navigation_config
    DB-->>API: Success
    API->>Store: Return response (new spaceIds if created)
    Store-->>NavUI: Mark changes as committed
    NavUI-->>Admin: Show success toast
    end

    rect rgb(240, 220, 200)
    Note over NavUI,Store: Alternative: Cancel Flow
    Admin->>NavUI: Click Cancel
    NavUI->>Store: Reset to remote state
    Store-->>NavUI: Restore original items
    NavUI-->>Admin: Show cancelled toast
    end
Loading
sequenceDiagram
    participant Server as Server (page.tsx)
    participant DB as Database
    participant Client as NavPageSpace (Client)
    participant LocalStore as Local Stores

    rect rgb(220, 240, 220)
    Note over Server,LocalStore: Space Data Fallback Flow
    Server->>DB: Query spaceData by navSlug
    alt Space Data Found
        DB-->>Server: Return spaceData
        Server->>Client: Pass spaceData to component
    else Space Data Missing
        DB-->>Server: No data
        Server->>Server: Load adminIdentityPublicKeys
        Server->>Client: Pass null spaceData + adminIdentityPublicKeys
        Client->>LocalStore: Query localNavItem & localSpace
        LocalStore-->>Client: Return local navigation/space items
        Client->>Client: Construct NavPageSpacePageData
        Client->>Client: Render with local data + isEditable
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring close attention:

  • navigationStore.ts: Complex commit logic with API interaction, signed payloads, space registration, identity validation, and error handling fallbacks
  • api/navigation/config.ts: Authorization checks using adminIdentityPublicKeys, signature validation, and database update semantics
  • Navigation.tsx & NavigationManagement.tsx: Interactive state management, debounced drag-and-drop reordering, inline editing, and event propagation control
  • NavPageSpace.tsx & page.tsx: Client-side fallback logic and data reconstruction from local stores; interaction between server and client data sources
  • Event propagation in editable-text.tsx: Verify click handlers prevent unintended navigation during edit workflows across all UI contexts

Possibly related PRs

  • Admin Editor #1619: Overlapping nav-page admin editing changes including adminIdentityPublicKeys, NAV_PAGE space type, NavPageSpace utilities, navigation store, API routes, and seed.ts modifications.

Poem

🐰 A rabbit hops through nav-igation's maze,
Staging changes in a local daze,
With draggable items, edit mode on,
Commits signed changes 'til morning's dawn,
No server? No worries—we'll build it our way! 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Nav editor' is vague and generic. While it relates to navigation editing features introduced in the PR, it lacks specificity and doesn't clearly convey the main changes (adding admin-level navigation editing, local fallbacks, and API endpoints). Consider a more descriptive title like 'Add admin navigation editing with client-side fallbacks and API endpoint' to clearly communicate the main changes to reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

* Requires:
* - NEXT_PUBLIC_SUPABASE_URL
* - SUPABASE_SERVICE_ROLE_KEY
* - SUPABASE_SERVICE_KEY
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for references to both variable names
echo "=== Searching for SUPABASE_SERVICE_ROLE_KEY ==="
rg "SUPABASE_SERVICE_ROLE_KEY" -n --type-list | head -20

echo -e "\n=== Searching for SUPABASE_SERVICE_KEY ==="
rg "SUPABASE_SERVICE_KEY" -n --type-list | head -20

# Look for .env files
echo -e "\n=== Checking for .env files ==="
fd -e env -o -name ".env*" | head -20

# Check the seed.ts file to understand context
echo -e "\n=== Content of scripts/seed.ts (lines 15-75) ==="
wc -l scripts/seed.ts

Repository: blankdotspace/space-system

Length of output: 1292


🏁 Script executed:

#!/bin/bash
# Search for references to both variable names
echo "=== Searching for SUPABASE_SERVICE_ROLE_KEY ==="
rg "SUPABASE_SERVICE_ROLE_KEY" --type ts --type js --type md -n 2>/dev/null | head -30

echo -e "\n=== Searching for SUPABASE_SERVICE_KEY ==="
rg "SUPABASE_SERVICE_KEY" --type ts --type js --type md -n 2>/dev/null | head -30

# Look for .env files
echo -e "\n=== Checking for .env files ==="
fd "\.env" --type f 2>/dev/null

# Check the seed.ts file content around the changes
echo -e "\n=== Content of scripts/seed.ts (lines 15-75) ==="
sed -n '15,75p' scripts/seed.ts

Repository: blankdotspace/space-system

Length of output: 3106


Incomplete environment variable migration: update src/config/loaders/runtimeLoader.ts and documentation.

The rename from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY is inconsistently applied. The seed script, tests, and production code (src/common/data/database/supabase/clients/server.ts) use the new name, but src/config/loaders/runtimeLoader.ts still references the old name. This will cause runtime failures.

Update:

  • src/config/loaders/runtimeLoader.ts:17 to use SUPABASE_SERVICE_KEY
  • docs/GETTING_STARTED.md:72 and docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md:466 to reflect the new variable name
🤖 Prompt for AI Agents
In scripts/seed.ts around line 20 and specifically in
src/config/loaders/runtimeLoader.ts at line 17, the environment variable rename
was applied inconsistently: runtimeLoader.ts still references
SUPABASE_SERVICE_ROLE_KEY while the rest of the code uses SUPABASE_SERVICE_KEY;
update runtimeLoader.ts to read SUPABASE_SERVICE_KEY instead of
SUPABASE_SERVICE_ROLE_KEY. Also update docs/GETTING_STARTED.md at line 72 and
docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md at line 466 to replace any
occurrences of SUPABASE_SERVICE_ROLE_KEY with SUPABASE_SERVICE_KEY so
documentation matches runtime usage.

Comment on lines +146 to +148
React.useEffect(() => {
loadNavigation(systemConfig.navigation);
}, [systemConfig.navigation, loadNavigation]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Navigation component and check how it receives systemConfig
find . -name "Navigation.tsx" -type f | head -5

Repository: blankdotspace/space-system

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Read the Navigation.tsx file to understand the useEffect and how systemConfig is used
cat -n ./src/common/components/organisms/Navigation.tsx | head -200

Repository: blankdotspace/space-system

Length of output: 9026


🏁 Script executed:

#!/bin/bash
# Continue reading to understand how systemConfig is passed to Navigation component
cat -n ./src/common/components/organisms/Navigation.tsx | sed -n '200,300p'

Repository: blankdotspace/space-system

Length of output: 3882


🏁 Script executed:

#!/bin/bash
# Find where Navigation component is used and how systemConfig is passed to it
rg -n "Navigation" --type=tsx -B 2 -A 2 | grep -A 5 -B 5 "systemConfig" | head -50

Repository: blankdotspace/space-system

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Find where Navigation component is used and how systemConfig is passed to it
rg -n "<Navigation" -A 5 | head -60

Repository: blankdotspace/space-system

Length of output: 2659


🏁 Script executed:

#!/bin/bash
# Check the Sidebar component to see where systemConfig comes from
cat -n ./src/common/components/organisms/Sidebar.tsx | head -150

Repository: blankdotspace/space-system

Length of output: 3217


🏁 Script executed:

#!/bin/bash
# Find where Sidebar is used and how systemConfig is obtained
rg -n "<Sidebar" -A 3 | head -50

Repository: blankdotspace/space-system

Length of output: 2032


🏁 Script executed:

#!/bin/bash
# Check ClientSidebarWrapper to see how systemConfig is obtained
cat -n ./src/common/components/organisms/ClientSidebarWrapper.tsx

Repository: blankdotspace/space-system

Length of output: 704


🏁 Script executed:

#!/bin/bash
# Find where ClientSidebarWrapper is used and how systemConfig is passed
rg -n "ClientSidebarWrapper" -B 5 -A 5

Repository: blankdotspace/space-system

Length of output: 5342


🏁 Script executed:

#!/bin/bash
# Check layout.tsx to see how systemConfig is loaded
cat -n ./src/app/layout.tsx | head -160

Repository: blankdotspace/space-system

Length of output: 5726


🏁 Script executed:

#!/bin/bash
# Check how loadSystemConfig is defined
rg -n "loadSystemConfig" -A 10 | head -80

Repository: blankdotspace/space-system

Length of output: 4541


🏁 Script executed:

#!/bin/bash
# Check the actual loadSystemConfig implementation
cat -n ./src/config/index.ts | head -100

Repository: blankdotspace/space-system

Length of output: 4224


🏁 Script executed:

#!/bin/bash
# Check the navigationStore to understand what loadNavigation does
cat -n ./src/common/data/stores/app/navigation/navigationStore.ts | head -100

Repository: blankdotspace/space-system

Length of output: 4118


🏁 Script executed:

#!/bin/bash
# Check the RuntimeConfigLoader to understand if it returns a new object each time
cat -n ./src/config/loaders/runtimeLoader.ts | head -150

Repository: blankdotspace/space-system

Length of output: 3238


🏁 Script executed:

#!/bin/bash
# Check if there are any other places where Navigation might receive a newly created systemConfig
rg -n "systemConfig\." ./src/app/layout.tsx | head -20

Repository: blankdotspace/space-system

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Check how many times systemConfig could be regenerated during a page lifecycle
# Look for any callbacks or effects that might reload config
rg -n "loadSystemConfig\|revalidate" ./src/app/layout.tsx

Repository: blankdotspace/space-system

Length of output: 52


Object reference dependency in useEffect may cause unnecessary re-runs.

The effect depends on systemConfig.navigation, which could be a new object reference even when its contents are identical. This causes the effect to re-run and reset localNavigation, potentially discarding uncommitted user changes.

Consider depending on the actual navigation items instead of the object itself, or memoize the navigation config to ensure referential stability:

React.useEffect(() => {
  loadNavigation(systemConfig.navigation);
}, [systemConfig.navigation?.items, loadNavigation]);

Or use useMemo to stabilize the reference:

const stableNavigation = React.useMemo(
  () => systemConfig.navigation,
  [systemConfig.navigation?.items]
);

React.useEffect(() => {
  loadNavigation(stableNavigation);
}, [stableNavigation, loadNavigation]);
🤖 Prompt for AI Agents
In src/common/components/organisms/Navigation.tsx around lines 146-148, the
useEffect depends on systemConfig.navigation (an object) which may get a new
reference and cause unnecessary re-runs and reset localNavigation; update the
dependency to a stable value by either depending on the actual navigation items
(e.g., systemConfig.navigation?.items) or memoizing the navigation config with
useMemo keyed by navigation items and then use that stable value in the effect;
adjust the useEffect dependency array to use the chosen stable reference and
ensure loadNavigation receives the memoized/stable navigation.

Comment on lines +159 to +164
const debouncedUpdateOrder = React.useCallback(
debounce((newOrder: NavigationItem[]) => {
updateNavigationOrder(newOrder);
}, 300),
[updateNavigationOrder]
);
Copy link
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 on lines +213 to +249
// TODO: For local testing, skip space registration if API fails
let actualSpaceId = space.spaceId;
try {
const { data: registrationResponse } = await axiosBackend.post<RegisterNewSpaceResponse>("/api/space/registry", signedRegistration);

if (registrationResponse.result !== "success" || !registrationResponse.value?.spaceId) {
throw new Error(`Failed to register space for nav item ${space.itemId}`);
}

actualSpaceId = registrationResponse.value!.spaceId;

// Update the navigation item and space store with the actual spaceId from the API
set((draft) => {
const itemIndex = draft.navigation.localNavigation.findIndex(
(item) => item.id === space.itemId
);
if (itemIndex !== -1) {
draft.navigation.localNavigation[itemIndex].spaceId = actualSpaceId;
}
// Move the localSpaces entry to the new spaceId (if spaceId changed)
if (space.spaceId !== actualSpaceId && draft.space.localSpaces[space.spaceId]) {
draft.space.localSpaces[actualSpaceId] = draft.space.localSpaces[space.spaceId];
draft.space.localSpaces[actualSpaceId].id = actualSpaceId;
delete draft.space.localSpaces[space.spaceId];
} else if (space.spaceId === actualSpaceId) {
// Ensure the spaceId is set correctly
draft.space.localSpaces[actualSpaceId].id = actualSpaceId;
}
}, "commitNavigationChanges-updateSpaceId");

// Use spaceStore's commitAllSpaceChanges to commit the tab order and sync state
// This handles tab commits, deletions, and order updates, and syncs remoteSpaces
await get().space.commitAllSpaceChanges(actualSpaceId);
} catch (spaceError: any) {
// For local testing, continue even if space registration fails
console.warn(`Space registration failed for nav item ${space.itemId} - continuing with local spaceId:`, spaceError);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure mode could lead to inconsistent state.

When space registration fails (lines 246-249), the code logs a warning and continues. However, the navigation item still has the local spaceId which won't exist in the database. On page reload, the navigation will reference a non-existent space.

Consider either:

  1. Removing the spaceId from the navigation item if registration fails
  2. Not committing the navigation config if any space registration fails
  3. Clearly documenting this as "local-only" preview behavior
🔎 Proposed fix (option 1)
      } catch (spaceError: any) {
        // For local testing, continue even if space registration fails
        console.warn(`Space registration failed for nav item ${space.itemId} - continuing with local spaceId:`, spaceError);
+       // Remove spaceId from navigation item since it won't exist in database
+       set((draft) => {
+         const itemIndex = draft.navigation.localNavigation.findIndex(
+           (item) => item.id === space.itemId
+         );
+         if (itemIndex !== -1) {
+           delete draft.navigation.localNavigation[itemIndex].spaceId;
+         }
+         // Clean up local space entry
+         delete draft.space.localSpaces[space.spaceId];
+       }, "commitNavigationChanges-cleanupFailedSpace");
      }
🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines
213-249, the catch block currently logs and continues which leaves the nav item
pointing to a local-only spaceId that won't exist remotely; update the catch to
remove the stale spaceId from the navigation item and clean up localSpaces
instead of silently continuing: find the navigation item by space.itemId and set
its spaceId to undefined/null (or remove the property), delete any
draft.space.localSpaces[space.spaceId] entry, and avoid calling
space.commitAllSpaceChanges for that failed space; optionally add a clear
"local-only" flag or comment to the entry so UI can show preview-only state.

Comment on lines +265 to +281
// Step 3: Update navigation config in database
const unsignedRequest = {
communityId,
navigationConfig: {
items: state.localNavigation,
},
publicKey,
timestamp: moment().toISOString(),
};

const signedRequest = signSignable(
unsignedRequest,
currentIdentity.rootKeys.privateKey
);

// Update navigation config in database
await axiosBackend.put("/api/navigation/config", signedRequest);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Navigation config update doesn't include all NavigationConfig fields.

The update request only sends items but NavigationConfig also has logoTooltip, showMusicPlayer, and showSocials. If these exist in the current config, they'll be overwritten with undefined.

Based on the type definition in systemConfig.ts (lines 154-159), ensure the full config is preserved:

🔎 Proposed fix
    // Step 3: Update navigation config in database
+   // Preserve other navigation config fields (logoTooltip, showMusicPlayer, showSocials)
+   // by only updating the items array
    const unsignedRequest = {
      communityId,
      navigationConfig: {
        items: state.localNavigation,
+       // TODO: Include existing config values or fetch them from server
+       // For now, only items are updated - other fields should be preserved server-side
      },
      publicKey,
      timestamp: moment().toISOString(),
    };

Alternatively, modify the API to merge rather than replace the entire config.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines 265 to
281, the unsignedRequest only includes navigationConfig.items which will cause
other NavigationConfig fields (logoTooltip, showMusicPlayer, showSocials) to be
lost; fix by building navigationConfig for the request from the full current
config (merge existing config fields with the updated items) so the
unsignedRequest includes items plus logoTooltip, showMusicPlayer, and
showSocials (or any other fields present) before signing and sending;
alternatively, if preferred, change the API to accept a partial update and merge
server-side rather than replacing the entire config.

Comment on lines 294 to 299
export function partializedNavigationStore(state: AppStore) {
return {
// Only persist minimal state if needed
// Navigation config is loaded from SystemConfig on app start
};
}
Copy link
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
# Search for partializedNavigationStore usage and verify the store configuration
rg -n "partializedNavigationStore" -A 10 -B 10

Repository: blankdotspace/space-system

Length of output: 2980


🏁 Script executed:

#!/bin/bash
# Check the navigationStore file structure and imports
fd -e ts -e tsx navigationStore | head -5 | xargs -I {} sh -c 'echo "File: {}"; wc -l {}; echo "---"'

Repository: blankdotspace/space-system

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Find how partializedNavigationStore is used in the persist configuration
rg -n "partializedNavigationStore\|persist.*navigation" src/common/data/stores/app/index.tsx -A 3 -B 3

Repository: blankdotspace/space-system

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Look for the full persist middleware configuration
grep -n "persist\|partialize" src/common/data/stores/app/index.tsx | head -20

Repository: blankdotspace/space-system

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Get the exact persist configuration around lines 109-116
sed -n '109,116p' src/common/data/stores/app/index.tsx

Repository: blankdotspace/space-system

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Check if navigationStore is included in the partialize config
sed -n '100,125p' src/common/data/stores/app/index.tsx

Repository: blankdotspace/space-system

Length of output: 835


Remove unused partializedNavigationStore function.

The function is imported in the app store configuration but never used in the persist middleware's partialize function (lines 109-114). Since navigation state is intentionally not persisted (loaded from SystemConfig on app start), either remove the unused partializedNavigationStore function and its import, or add navigation: partializedNavigationStore(state) to the partialize object if persistence is intended.

🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines
294-299, the exported partializedNavigationStore function is unused (imported in
app store config but not included in the persist partialize) and navigation
state is intentionally not persisted; either delete this function and remove its
import from the app store configuration, or if you intended to persist
navigation add navigation: partializedNavigationStore(state) to the persist
partialize object (lines ~109-114) and keep the export — choose one and update
the corresponding import/usage accordingly.

Comment on lines +14 to +19
export type UnsignedUpdateNavigationConfigRequest = {
communityId: string;
navigationConfig: NavigationConfig;
publicKey: string;
timestamp: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing timestamp validation to prevent replay attacks.

The request includes a timestamp field but it's never validated. An attacker could replay a previously captured signed request indefinitely. Consider adding timestamp freshness validation.

🔎 Proposed fix

Add timestamp validation after signature verification:

  if (!validateSignable(updateRequest)) {
    res.status(400).json({
      result: "error",
      error: {
        message: "Invalid signature",
      },
    });
    return;
  }

+ // Validate timestamp freshness (e.g., within 5 minutes)
+ const requestTime = new Date(updateRequest.timestamp).getTime();
+ const now = Date.now();
+ const maxAge = 5 * 60 * 1000; // 5 minutes
+ if (isNaN(requestTime) || Math.abs(now - requestTime) > maxAge) {
+   res.status(400).json({
+     result: "error",
+     error: {
+       message: "Request timestamp is invalid or expired",
+     },
+   });
+   return;
+ }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/api/navigation/config.ts around lines 14 to 19 the
UnsignedUpdateNavigationConfigRequest includes a timestamp but there is no
freshness check, enabling replay attacks; after verifying the signature parse
and validate the timestamp (accept ISO or epoch), compute the age against server
time, reject requests older or too-future skew beyond a configured window (e.g.,
5 minutes) with an appropriate 4xx response, centralize the allowed skew value
as a constant and add robust parsing/error handling for malformed timestamps so
only fresh, well-formed requests proceed.

Comment on lines 99 to 106
const { error: updateError } = await supabase
.from("community_configs")
.update({
navigation_config: updateRequest.navigationConfig,
updated_at: new Date().toISOString(),
})
.eq("community_id", updateRequest.communityId)
.eq("is_published", true);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update operation lacks row-level check for successful update.

The update query doesn't verify that a row was actually updated. If the community_id and is_published combination doesn't match any row (race condition where config was unpublished), the update silently succeeds without modifying anything.

🔎 Proposed fix
  // Update the navigation_config column
- const { error: updateError } = await supabase
+ const { error: updateError, count } = await supabase
    .from("community_configs")
    .update({
      navigation_config: updateRequest.navigationConfig,
      updated_at: new Date().toISOString(),
    })
    .eq("community_id", updateRequest.communityId)
-   .eq("is_published", true);
+   .eq("is_published", true)
+   .select();

- if (updateError) {
+ if (updateError || count === 0) {
    console.error("Error updating navigation config:", updateError);
    res.status(500).json({
      result: "error",
      error: {
-       message: updateError.message,
+       message: updateError?.message || "No matching config found to update",
      },
    });
    return;
  }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/api/navigation/config.ts around lines 99 to 106, the UPDATE call
doesn't verify that a row was actually modified; change the code to request the
updated rows (e.g. include a .select() or use Supabase's returning behavior) and
then check the response: if updateError is set or the returned data array is
empty (no rows updated), treat this as a failure (log and return an error/409 or
throw) instead of proceeding silently; ensure the handler surface returns a
clear error when zero rows are affected so callers can handle the
race/unpublished case.

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