Skip to content

add left toolbar and show the graph schema#80

Merged
gkorland merged 11 commits intomainfrom
toolbar
Aug 21, 2025
Merged

add left toolbar and show the graph schema#80
gkorland merged 11 commits intomainfrom
toolbar

Conversation

@gkorland
Copy link
Contributor

@gkorland gkorland commented Aug 19, 2025

Summary by CodeRabbit

  • New Features

    • Left toolbar (Instructions, Theme, Schema), Schema sidebar and interactive DB graph visualization; new user-scoped API endpoint to fetch graph data.
  • UI / Style

    • Theme toggle moved into left toolbar; side-menu toggle removed; sidebar layout updated to include analytics and responsive behavior.
  • Accessibility

    • Keyboard navigation (roving tabindex) for toolbar; only one sidebar may be open at a time.
  • Chores

    • Added external graph rendering libraries.

@vercel
Copy link

vercel bot commented Aug 19, 2025

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

Project Deployment Preview Comments Updated (UTC)
queryweaver Canceled Canceled Aug 20, 2025 8:45pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Walkthrough

Adds an authenticated GET endpoint to return user-scoped graph nodes/links JSON, introduces a left vertical toolbar and schema sidebar, adds a Force Graph renderer (client), refactors sidebar toggling to multi-container control with keyboard-accessible toolbar, and updates templates/CSS to integrate the new UI.

Changes

Cohort / File(s) Summary
Backend: Graph data endpoint
api/routes/graphs.py
Adds sanitize_log_input and get_graph_data at GET /graphs/<graph_id>/data (token_required). Validates/limits graph_id, namespaces by user, loads graph, runs Cypher queries for tables and FK links, normalizes columns, deduplicates links, logs sanitized errors, and returns {"nodes": [...], "links": [...]} with 400/404/500 handling.
Client: App wiring & data flow
api/static/js/app.js
Replaces toggleMenu usage with toggleContainer(container); adds loadAndShowGraph(selected) to fetch /graphs/{id}/data, validate shape, and call showGraph(data); toggles schema panel on click/change and calls setupToolbar() during init.
Client: Schema visualization
api/static/js/modules/schema.js
New showGraph(data) module rendering ForceGraph into #schema-graph using a custom nodeCanvasObject (table box layout, header, columns/types), link/arrow rendering with theme-aware color, and D3-configured forces when available.
Client: UI module & accessibility
api/static/js/modules/ui.js
Renames toggleMenu()toggleContainer(container, onOpen) to operate on arbitrary containers and close other .sidebar-container elements; preserves desktop padding logic; adds setupToolbar() implementing roving tabindex and keyboard nav; updates resize handling to consider multiple sidebars.
Client: Config selectors/DOM
api/static/js/modules/config.js
Removes sideMenuButton selector/DOM lookup; adds schemaButton, schemaContainer, leftToolbar, toolbarButtons, and leftToolbarInner selectors and DOM lookups.
Client: Public API exports
api/static/js/modules/schema.js, api/static/js/modules/ui.js
Adds showGraph(data) export and replaces toggleMenu export with toggleContainer(container, onOpen); adds setupToolbar() export.
Templates: layout and components
api/templates/base.j2, api/templates/chat.j2, api/templates/components/left_toolbar.j2, api/templates/components/sidebar_schema.j2, api/templates/components/sidebar_menu.j2, api/templates/components/toolbar.j2
base.j2 adds D3 & Force Graph CDN scripts. chat.j2 includes left_toolbar and sidebar_schema, removes legacy side-menu toggle. Adds left_toolbar.j2 and sidebar_schema.j2. sidebar_menu.j2 converted to .sidebar-container and header/close removed; analytics integrated. toolbar.j2 theme toggle removed (moved to left toolbar).
CSS: toolbar, schema panel, menu, responsive
api/static/css/buttons.css, api/static/css/menu.css, api/static/css/responsive.css
buttons.css: theme toggle moved to flow and adds #left-toolbar, .toolbar-button, #toolbar-buttons, footer, and responsive behavior. menu.css: #menu-container.sidebar-container, removed header/side-menu controls, added #schema-graph and canvas sizing rules. responsive.css: updated selectors to .sidebar-container, removed side-menu and theme-toggle responsive positioning, adjusted GitHub button offsets.
JS: app config changes
api/static/js/modules/config.js, api/static/js/app.js
Config selectors updated and app initialization calls setupToolbar(); graph fetches encode graph id and perform shape validation before rendering.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Browser as Browser UI
  participant API as GET /graphs/{id}/data
  participant DB as Database

  User->>Browser: Click "Schema" button
  Browser->>Browser: toggleContainer(schemaContainer, onOpen)
  alt opened & graph selected
    Browser->>API: GET /graphs/{encodedId}/data (auth token)
    API->>DB: select_graph(namespaced_id) + Cypher queries
    DB-->>API: nodes & links
    API-->>Browser: 200 {nodes, links}
    Browser->>Browser: validate shape
    Browser->>Browser: showGraph(data)
  else no graph selected
    Browser->>Browser: skip fetch
  else API/db error
    Browser->>Browser: log error / skip rendering
  end
Loading
sequenceDiagram
  participant UI as Browser UI
  participant SB as .sidebar-container[*]

  UI->>SB: toggleContainer(target, onOpen?)
  Note right of SB: Close other .sidebar-container instances
  alt Opening
    SB->>SB: add .open (desktop padding adjust)
    SB-->>UI: optional onOpen callback executed (e.g., load graph)
  else Closing
    SB->>SB: remove .open (restore padding)
  end
  UI->>UI: window resize -> adjust padding for each sidebar
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hop and nudge a sidebar bright,
Canvas tables wake to light,
Arrows whisper links in rows,
Keys let tiny focus hops and flows,
A rabbit claps: the schema's right! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch toolbar

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 19, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@gkorland gkorland requested a review from galshubeli August 20, 2025 18:44
@gkorland gkorland marked this pull request as ready for review August 20, 2025 18:44
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: 11

🧹 Nitpick comments (20)
api/templates/base.j2 (1)

19-20: Pin CDN versions, add defer, and consider SRI/CSP alignment

Unpinned CDN URLs can break unexpectedly on upstream releases and block rendering. Recommend pinning versions and deferring script load. If you enforce a CSP, make sure to allow these origins or prefer local hosting with SRI.

Apply this diff:

-    <script src="https://unpkg.com/d3"></script>
-    <script src="https://unpkg.com/force-graph"></script>
+    <script defer src="https://unpkg.com/d3@7"></script>
+    <script defer src="https://unpkg.com/force-graph@1"></script>

Optional hardening (fill in integrity hashes if you stick with CDN):

<script defer src="https://unpkg.com/d3@7" integrity="<!-- sha384-... -->" crossorigin="anonymous"></script>
<script defer src="https://unpkg.com/force-graph@1" integrity="<!-- sha384-... -->" crossorigin="anonymous"></script>

Please verify:

  • Your CSP policy allows these domains (script-src) or switch to self-hosted copies under api/static/.
  • No race with modules that expect ForceGraph/d3 before DOMContentLoaded (using defer should be fine).
api/static/js/modules/schema.js (2)

4-5: Prevent multiple canvases on repeated renders

If showGraph is called multiple times, force-graph will append another canvas each time. Clear the container or reuse a singleton instance.

Apply this diff:

-    const Graph = ForceGraph()(document.getElementById('schema-graph'))
+    const container = document.getElementById('schema-graph');
+    container.textContent = '';
+    const Graph = ForceGraph()(container)

Alternative: keep a module-level singleton Graph and call Graph.graphData(data) to update instead of recreating.


72-73: Reduce console noise

Consider removing or gating the console log behind a debug flag.

api/templates/components/sidebar_schema.j2 (1)

2-6: Add ARIA roles/labels for accessibility

Mark the schema sidebar as a complementary region with a descriptive label.

Apply this diff:

-<div id="schema-container" class="sidebar-container">
+<div id="schema-container" class="sidebar-container" role="complementary" aria-label="Schema panel">
     <div id="schema-content">
-        <div id="schema-graph"></div>
+        <div id="schema-graph" aria-label="Graph visualization area"></div>
     </div>
 </div>
api/templates/components/sidebar_menu.j2 (1)

2-2: Verify selector consistency for the menu container

You’re using id="menu-container" plus class="sidebar-container". Ensure the JS toggling logic and CSS use the updated class-based selector, or keep the id usage consistent in config/ui modules to avoid regressions.

If moving fully to class-based selectors, consider removing the id for clarity:

-<div id="menu-container" class="sidebar-container">    
+<div class="sidebar-container">    
api/templates/components/left_toolbar.j2 (3)

14-21: Use aria-expanded + aria-controls (not aria-pressed) for the toggle button, and wire it to the controlled container

The “Instructions” button toggles a sidebar. For accessibility, use aria-expanded and aria-controls. aria-pressed is for on/off buttons (e.g., theme).

-            <button class="action-button toolbar-button" id="menu-button" title="Instructions" aria-label="Instructions"
-                aria-pressed="false" tabindex="0">
+            <button class="action-button toolbar-button" id="menu-button" title="Instructions" aria-label="Instructions"
+                aria-expanded="false" aria-controls="menu-container" tabindex="0">

43-56: Schema button should also use aria-expanded + aria-controls and maintain state

Same reasoning as the menu button: this opens/closes a panel, so aria-expanded and aria-controls are preferable.

-            <button class="action-button toolbar-button" id="schema-button" title="Schema"
-                aria-label="Open schema" aria-pressed="false" tabindex="-1">
+            <button class="action-button toolbar-button" id="schema-button" title="Schema"
+                aria-label="Open schema" aria-expanded="false" aria-controls="schema-container" tabindex="-1">

5-10: Minor: move presentational inline styles to CSS

The brand slot uses inline styles. Consider a small CSS rule to keep styles co-located and easier to theme.

api/routes/graphs.py (6)

71-74: Docstring says “edges” but API returns “links”

Front-end expects links. Align the docstring to avoid confusion.

-    This endpoint returns a JSON object with two keys: `nodes` and `edges`.
-    Nodes contain a minimal set of properties (id, name, labels, props).
-    Edges contain source and target node names (or internal ids), type and props.
+    This endpoint returns a JSON object with two keys: `nodes` and `links`.
+    Nodes contain a minimal set of properties (id, name, columns).
+    Links contain source and target table names.

75-80: Inconsistent graph_id length limit across endpoints

This endpoint truncates to 200 chars, while others (e.g., POST /<graph_id>) use 100. Consider a single constant to avoid divergent behavior.

Would you like me to refactor to a shared MAX_GRAPH_ID_LEN constant and update all routes accordingly?


81-86: Address CodeQL “log injection” by sanitizing user-controlled values in logs

namespaced includes user input and e can carry newlines. Sanitize before logging.

-    except Exception as e:
-        logging.error("Failed to select graph %s: %s", namespaced, e)
+    except Exception as e:
+        safe_ns = namespaced.replace("\n", " ").replace("\r", " ")[:200]
+        safe_err = str(e).replace("\n", " ").replace("\r", " ")[:500]
+        logging.error("Failed to select graph %r: %s", safe_ns, safe_err)
         return jsonify({"error": "Graph not found or database error"}), 404

101-107: Also sanitize on query failure logs

Mirror the same sanitization for consistency and to silence the CodeQL finding.

-    except Exception as e:
-        logging.error("Error querying graph data for %s: %s", namespaced, e)
+    except Exception as e:
+        safe_ns = namespaced.replace("\n", " ").replace("\r", " ")[:200]
+        safe_err = str(e).replace("\n", " ").replace("\r", " ")[:500]
+        logging.error("Error querying graph data for %r: %s", safe_ns, safe_err)
         return jsonify({"error": "Failed to read graph data"}), 500

108-123: Normalize columns to exclude nulls and enforce list shape

collect(DISTINCT c.name) may include nulls. Filter them to prevent UI oddities.

-        # columns may contain nulls if no columns — normalize to empty list
-        if not isinstance(columns, list):
-            columns = [] if columns is None else [columns]
+        # Normalize columns: ensure a list and strip nulls
+        if isinstance(columns, list):
+            columns = [c for c in columns if c]
+        else:
+            columns = [] if columns is None else [columns]  # rare driver edge-case

87-99: Be mindful of large graphs: add safeguards or pagination

For large schemas, returning all tables/links can be heavy for both DB and client. Consider:

  • adding optional limits/filters (e.g., max nodes/links),
  • or server-side paging,
  • or feature-gating expensive queries.

If you want, I can add a max_nodes/max_links query param and hard cap on server, plus a small “truncated” flag in the response so the UI can message it.

Also applies to: 101-107, 124-137

api/static/css/responsive.css (1)

15-18: Align selector with new .sidebar-container naming (or keep both for compatibility)

This still targets #menu-container. Since the rest migrated to .sidebar-container, consider updating or supporting both.

-  #menu-container.open~#chat-container {
+  .sidebar-container.open ~ #chat-container,
+  #menu-container.open ~ #chat-container {
     margin-left: 0;
   }
api/static/js/app.js (1)

48-49: Keep the menu button’s ARIA state in sync

After toggling, update aria-expanded to reflect the actual state.

-    DOM.menuButton.addEventListener('click', () => toggleContainer(DOM.menuContainer));
+    DOM.menuButton.addEventListener('click', () => {
+        toggleContainer(DOM.menuContainer);
+        const expanded = DOM.menuContainer.classList.contains('open');
+        DOM.menuButton.setAttribute('aria-expanded', expanded.toString());
+    });
api/static/js/modules/config.js (1)

21-29: Remove stale sideMenuButton selector to avoid drift.

sideMenuButton remains in SELECTORS but is not mirrored in DOM and appears deprecated with the new toolbar. Keeping it invites confusion and dead references.

Apply this diff to remove it:

 export const SELECTORS = {
     messageInput: '#message-input',
     submitButton: '#submit-button',
     pauseButton: '#pause-button',
     newChatButton: '#reset-button',
     chatMessages: '#chat-messages',
     expValue: '#exp-value',
     confValue: '#conf-value',
     missValue: '#info-value',
     ambValue: '#amb-value',
     fileUpload: '#schema-upload',
     fileLabel: '#custom-file-upload',
-    sideMenuButton: '#side-menu-button',
     menuButton: '#menu-button',
     schemaButton: '#schema-button',
     menuContainer: '#menu-container',
     schemaContainer: '#schema-container',
     chatContainer: '#chat-container',
     leftToolbar: '#left-toolbar',
     toolbarButtons: '#toolbar-buttons',
     leftToolbarInner: '#left-toolbar-inner',
     expInstructions: '#instructions-textarea',
api/static/js/modules/ui.js (2)

19-39: Optional: keep controlling button states in sync (aria-pressed).

When closing other sidebars, consider resetting any toolbar buttons with aria-pressed="true" so assistive tech reflects UI state. Can be done cheaply without hard coupling.

Example addition (after removing 'open' from other containers):

 allContainers.forEach((c) => {
     if (c !== container && c.classList.contains('open')) {
         c.classList.remove('open');
+        // Reset any pressed toolbar buttons
+        document.querySelectorAll('.toolbar-button[aria-pressed="true"]').forEach(btn => {
+            btn.setAttribute('aria-pressed', 'false');
+        });
     }
 });

176-225: Remove commented-out experimental code.

Large commented block adds noise and will rot. Consider deleting or moving into docs if needed later.

Apply this diff to remove the block:

-    // const schemaBtn = document.getElementById('toolbar-schema');
-    // let schemaPanel = document.getElementById('schema-panel');
-    //
-    // if (!schemaPanel) {
-    //     schemaPanel = document.createElement('div');
-    //     schemaPanel.id = 'schema-panel';
-    //     schemaPanel.innerHTML = '<h3 style="margin:0 0 6px 0;font-size:14px;">Schema</h3><div id="schema-panel-body" style="margin-top:8px;font-size:13px;">Schema panel (empty). Add schema widgets here.</div>';
-    //     document.body.appendChild(schemaPanel);
-    // }
-    //
-    // async function loadSchemaIntoPanel() {
-    //     const body = document.getElementById('schema-panel-body');
-    //     if (!body) return;
-    //     body.innerHTML = '<p style="font-size:13px;margin:0;">Loading schema…</p>';
-    //     try {
-    //         const resp = await fetch('/api/schema');
-    //         if (!resp.ok) throw new Error('Network response was not ok');
-    //         const data = await resp.json();
-    //         // If data has tables array, render a simple list/table
-    //         if (Array.isArray(data.tables)) {
-    //             const ul = document.createElement('ul');
-    //             ul.style.margin = '0';
-    //             ul.style.padding = '0 0 0 14px';
-    //             data.tables.forEach(t => {
-    //                 const li = document.createElement('li');
-    //                 li.textContent = t.name || JSON.stringify(t);
-    //                 ul.appendChild(li);
-    //             });
-    //             body.innerHTML = '';
-    //             body.appendChild(ul);
-    //         } else {
-    //             body.innerHTML = '<pre style="white-space:pre-wrap;font-size:12px;margin:0;">' + JSON.stringify(data, null, 2) + '</pre>';
-    //         }
-    //     } catch (err) {
-    //         body.innerHTML = '<p style="color:var(--text-secondary);margin:0;font-size:13px;">Could not load schema. Showing placeholder.</p><pre style="white-space:pre-wrap;font-size:12px;margin-top:8px;">' + String(err) + '</pre>';
-    //     }
-    // }
-    //
-    // if (schemaBtn) {
-    //     schemaBtn.addEventListener('click', function() {
-    //         const isOpen = schemaBtn.getAttribute('aria-pressed') === 'true';
-    //         schemaBtn.setAttribute('aria-pressed', (!isOpen).toString());
-    //         schemaPanel.classList.toggle('open');
-    //         if (!isOpen) {
-    //             // panel opened — attempt to load schema
-    //             loadSchemaIntoPanel();
-    //         }
-    //     });
-    // }
api/static/css/buttons.css (1)

298-300: Remove duplicate comment line.

Apply this diff:

-/* Left vertical toolbar (activity bar style) */
-/* Left vertical toolbar (activity bar style) */
+/* Left vertical toolbar (activity bar style) */
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aebeb04 and 4fc9370.

📒 Files selected for processing (14)
  • api/routes/graphs.py (1 hunks)
  • api/static/css/buttons.css (2 hunks)
  • api/static/css/menu.css (3 hunks)
  • api/static/css/responsive.css (1 hunks)
  • api/static/js/app.js (3 hunks)
  • api/static/js/modules/config.js (2 hunks)
  • api/static/js/modules/schema.js (1 hunks)
  • api/static/js/modules/ui.js (2 hunks)
  • api/templates/base.j2 (1 hunks)
  • api/templates/chat.j2 (1 hunks)
  • api/templates/components/left_toolbar.j2 (1 hunks)
  • api/templates/components/sidebar_menu.j2 (1 hunks)
  • api/templates/components/sidebar_schema.j2 (1 hunks)
  • api/templates/components/toolbar.j2 (0 hunks)
💤 Files with no reviewable changes (1)
  • api/templates/components/toolbar.j2
🧰 Additional context used
📓 Path-based instructions (4)
api/templates/**

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store Jinja2 templates under api/templates/

Files:

  • api/templates/chat.j2
  • api/templates/base.j2
  • api/templates/components/sidebar_schema.j2
  • api/templates/components/left_toolbar.j2
  • api/templates/components/sidebar_menu.j2
api/static/**

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store frontend static assets under api/static/

Files:

  • api/static/js/modules/schema.js
  • api/static/css/responsive.css
  • api/static/js/modules/config.js
  • api/static/js/modules/ui.js
  • api/static/css/buttons.css
  • api/static/css/menu.css
  • api/static/js/app.js
api/routes/graphs.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Keep graph/database route handlers in api/routes/graphs.py

Files:

  • api/routes/graphs.py
api/routes/**/*.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Place all additional Flask route handlers under api/routes/

Files:

  • api/routes/graphs.py
🧬 Code Graph Analysis (3)
api/routes/graphs.py (1)
api/auth/user_management.py (1)
  • token_required (263-285)
api/static/js/modules/ui.js (1)
api/static/js/modules/config.js (2)
  • DOM (39-65)
  • DOM (39-65)
api/static/js/app.js (3)
api/static/js/modules/config.js (2)
  • DOM (39-65)
  • DOM (39-65)
api/static/js/modules/ui.js (2)
  • toggleContainer (7-39)
  • setupToolbar (141-225)
api/static/js/modules/schema.js (1)
  • showGraph (2-78)
🪛 GitHub Check: CodeQL
api/routes/graphs.py

[failure] 84-84: Log Injection
This log entry depends on a user-provided value.


[failure] 105-105: Log Injection
This log entry depends on a user-provided value.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e-tests
  • GitHub Check: e2e-tests
🔇 Additional comments (9)
api/templates/chat.j2 (1)

11-14: LGTM: Left toolbar and schema sidebar wiring looks correct

Includes are placed early in the container and should layer well with the chat content, assuming CSS sets appropriate z-index and widths.

api/templates/components/left_toolbar.j2 (1)

23-41: Theme toggle markup looks good

Inline SVG covers both sun/moon/system glyphs and the button is correctly set as a toggle (aria-pressed). This matches a toggle control semantics.

api/static/css/responsive.css (1)

22-33: Verified: all menu/schema panels include class="sidebar-container"
Our scan confirmed that every element with id="menu-container" or id="schema-container" already has class="sidebar-container". No further changes required.

api/static/js/app.js (2)

10-18: Nice: unified container toggle API and toolbar setup

switching to toggleContainer and adding setupToolbar makes the left toolbar keyboard accessible.

Also applies to: 112-118


50-84: Ensure a valid fallback dataset and synchronize ARIA state

I didn’t find any myData definition in your JS files. If it’s injected globally (e.g., in your HTML), please verify it’s available before this script runs—otherwise every showGraph(myData) call will throw a ReferenceError. Consider providing a safe inline fallback or centralizing a demo dataset. Also, after toggling the panel, update the button’s aria-expanded attribute to match its open/closed state.

• Confirm or define myData (or replace it with a proper fallback)
• Replace each showGraph(myData) with something like showGraph({ nodes: [], links: [] }) or a reference to your demo dataset
• After toggleContainer(…), add:

const expanded = DOM.schemaContainer.classList.contains('open');
DOM.schemaButton.setAttribute('aria-expanded', expanded.toString());

Example diff:

     try {
         const resp = await fetch(...);
         if (!resp.ok) {
             console.error('Failed to load graph data:', resp.status, resp.statusText);
-            showGraph(myData);
+            showGraph({ nodes: [], links: [] });
             return;
         }

         const data = await resp.json();
         if (!data || !Array.isArray(data.nodes) || !Array.isArray(data.links)) {
             console.warn('Graph data in unexpected shape, using fallback', data);
-            showGraph(myData);
+            showGraph({ nodes: [], links: [] });
             return;
         }

         showGraph(data);
     } catch (err) {
         console.error('Error fetching graph data:', err);
-        showGraph(myData);
+        showGraph({ nodes: [], links: [] });
     }
-});
+});
+// Sync ARIA-expanded on the schema button
+const expanded = DOM.schemaContainer.classList.contains('open');
+DOM.schemaButton.setAttribute('aria-expanded', expanded.toString());
api/static/css/menu.css (1)

19-25: Good call: offsetting the open sidebar for the 48px left toolbar

The left: 48px aligns the opened panel with the new vertical toolbar.

api/static/js/modules/config.js (1)

52-58: All sidebar elements correctly include the sidebar-container class—no changes needed

  • Verified IDs schema-container and menu-container exist in:
    • api/templates/components/sidebar_schema.j2:2 (<div id="schema-container" class="sidebar-container">)
    • api/templates/components/sidebar_menu.j2:2 (<div id="menu-container" class="sidebar-container">)
  • Other toolbar elements (left-toolbar, toolbar-buttons, schema-button, etc.) are present as expected.

No updates required.

api/static/js/modules/ui.js (1)

7-39: No stale toggleMenu() references found
All calls to toggleMenu() have been successfully replaced by toggleContainer(). No further action is required.

api/static/css/buttons.css (1)

140-142: Confirm static placement of #theme-toggle-btn in left toolbar

The theme toggle is now rendered in api/templates/components/left_toolbar.j2 (line 23). With its CSS changed to position: static; margin: 0;, please:

  • Verify it aligns correctly alongside the other toolbar buttons (check container’s display/flex settings and spacing).
  • Confirm there are no layout shifts or overlap when the header/toolbar is fixed or sticky.
  • Adjust container or button-specific styles if needed to restore intended layout.

Comment on lines 300 to 318
#left-toolbar {
position: fixed;
top: 0px;
bottom: 0px;
width: 48px;
background: var(--bg-tertiary, rgba(255,255,255,0.02));
/* border-radius: 10px; */
padding: 6px 6px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
border: 1px solid var(--border-color, rgba(255,255,255,0.03));
backdrop-filter: blur(6px);
overflow: hidden;
}
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

Toolbar lacks a left offset; position may be unpredictable.

#left-toolbar is fixed with top/bottom but no left/right on desktop. Add a left offset so it reliably anchors to the viewport edge.

Apply this diff:

 #left-toolbar {
   position: fixed;
   top: 0px;
   bottom: 0px;
   width: 48px;
+  left: 12px;
📝 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
#left-toolbar {
position: fixed;
top: 0px;
bottom: 0px;
width: 48px;
background: var(--bg-tertiary, rgba(255,255,255,0.02));
/* border-radius: 10px; */
padding: 6px 6px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
border: 1px solid var(--border-color, rgba(255,255,255,0.03));
backdrop-filter: blur(6px);
overflow: hidden;
}
#left-toolbar {
position: fixed;
top: 0px;
bottom: 0px;
width: 48px;
left: 12px;
background: var(--bg-tertiary, rgba(255,255,255,0.02));
/* border-radius: 10px; */
padding: 6px 6px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
border: 1px solid var(--border-color, rgba(255,255,255,0.03));
backdrop-filter: blur(6px);
overflow: hidden;
}
🤖 Prompt for AI Agents
In api/static/css/buttons.css around lines 300 to 318, the #left-toolbar is
positioned fixed with top and bottom but no left/right so its horizontal
placement can be unpredictable; add a left offset (e.g., left: 0;) to reliably
anchor it to the viewport edge (optionally include right: auto; if needed for
overrides) and ensure any layout breakpoints or RTL handling are accounted for
where this selector is used.

Comment on lines +328 to +336
.toolbar-button {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve affordance and focus visibility for toolbar buttons.

Add pointer cursor, normalize border/background, and provide a visible focus indicator for keyboard users.

Apply this diff:

 .toolbar-button {
   width: 40px;
   height: 40px;
   border-radius: 8px;
   display: flex;
   align-items: center;
   justify-content: center;
   box-sizing: border-box;
+  cursor: pointer;
+  border: none;
+  background: transparent;
 }
+
+.toolbar-button:focus-visible {
+  outline: 2px solid var(--falkor-accent);
+  outline-offset: 2px;
+}
📝 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
.toolbar-button {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.toolbar-button {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
border: none;
background: transparent;
}
.toolbar-button:focus-visible {
outline: 2px solid var(--falkor-accent);
outline-offset: 2px;
}
🤖 Prompt for AI Agents
In api/static/css/buttons.css around lines 328 to 336, the .toolbar-button lacks
pointer affordance, normalized border/background and a visible keyboard focus
indicator; update the rule to add cursor: pointer, set a neutral background
(e.g. transparent or var(--btn-bg)), and reset border (border: none) while
preserving border-radius and box-sizing, then add a :focus-visible style that
provides a clear, accessible focus ring (for example a 2px solid outline or a
visible box-shadow using a high-contrast focus color and outline-offset) so
keyboard users get a clear indicator without affecting mouse interactions.

Comment on lines 384 to 398
#schema-panel {
position: fixed;
left: 76px;
top: 84px;
width: 420px;
background: var(--bg-primary, #121212);
border: 1px solid var(--border-color, rgba(255,255,255,0.04));
padding: 12px;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
display: none;
z-index: 1040;
}

#schema-panel.open { display: block; }
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm schema container ID matches JS (schemaContainer vs schema-panel).

CSS targets #schema-panel while config.js exports schemaContainer (#schema-container). If these refer to the same element, align IDs; if not, verify both are intentional.

Run this to inspect both IDs in templates:

If they’re the same component, prefer one consistent ID across CSS/JS to prevent drift.


🏁 Script executed:

#!/bin/bash
rg -n -C2 -g '!**/dist/**' -g '!**/build/**' --type-add 'j2:*.j2' --type j2 \
  'id="schema-panel"|id="schema-container"'

Length of output: 453


To confirm the JS reference, let’s locate where schemaContainer is defined:


🏁 Script executed:

#!/bin/bash
rg -n -C2 -g '!**/dist/**' -g '!**/build/**' --type js 'schemaContainer'

Length of output: 1297


Align Schema Panel ID Across CSS, JS, and Template

The CSS is targeting #schema-panel, but both your JS (config.js → schemaContainer: '#schema-container' / getElementById('schema-container')) and the Jinja template (<div id="schema-container">) use schema-container. This mismatch prevents the panel from ever being shown.

Files to update:

  • api/static/css/buttons.css (lines ~384–398): change selector from #schema-panel to #schema-container
  • api/static/js/modules/config.js: already references schema-container
  • api/templates/components/sidebar_schema.j2 (line 2): already defines id="schema-container"

Suggested CSS diff:

-#schema-panel {
+#schema-container {
   position: fixed;
   left: 76px;
   top: 84px;
   width: 420px;
   /* … */
}

-#schema-panel.open { display: block; }
+#schema-container.open { display: block; }
📝 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
#schema-panel {
position: fixed;
left: 76px;
top: 84px;
width: 420px;
background: var(--bg-primary, #121212);
border: 1px solid var(--border-color, rgba(255,255,255,0.04));
padding: 12px;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
display: none;
z-index: 1040;
}
#schema-panel.open { display: block; }
#schema-container {
position: fixed;
left: 76px;
top: 84px;
width: 420px;
background: var(--bg-primary, #121212);
border: 1px solid var(--border-color, rgba(255,255,255,0.04));
padding: 12px;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
display: none;
z-index: 1040;
}
#schema-container.open { display: block; }
🤖 Prompt for AI Agents
In api/static/css/buttons.css around lines 384–398 the CSS selector uses
#schema-panel while the JS and template use id="schema-container"; update the
selector and any related rules to #schema-container so the CSS targets the
actual DOM element, keeping the same declarations (position, sizing, background,
border, padding, radius, shadow, display, z-index) and the .open variant changed
to #schema-container.open.

Comment on lines 71 to 75
// Stop when layout stabilizes and set canvas size
Graph.onEngineStop(() => {
console.log('Engine stopped, layout stabilized.');
}).width(window.innerWidth).height(window.innerHeight);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Size to container instead of window to avoid overflow in the sidebar

The graph canvas should match the sidebar container, not the viewport. Use the container’s bounding box.

Apply this diff:

-    Graph.onEngineStop(() => {
-        console.log('Engine stopped, layout stabilized.');
-    }).width(window.innerWidth).height(window.innerHeight);
+    const el = document.getElementById('schema-graph');
+    const { width, height } = el.getBoundingClientRect();
+    Graph.onEngineStop(() => {
+        // console.debug('Schema graph: layout stabilized');
+    }).width(width).height(height);

Additionally, add a resize observer after this block to keep the canvas responsive when the sidebar opens/resizes:

// keep graph sized to container
const ro = new ResizeObserver(entries => {
  for (const entry of entries) {
    const cr = entry.contentRect;
    Graph.width(cr.width).height(cr.height);
  }
});
ro.observe(el);
🤖 Prompt for AI Agents
In api/static/js/modules/schema.js around lines 71 to 75, the graph is sized to
the viewport using window.innerWidth/innerHeight which causes overflow in the
sidebar; replace those calls with the sidebar container's bounding client rect
(use el.getBoundingClientRect().width/height) when setting Graph.width/height,
and after that block add a ResizeObserver that observes the container element
(el) and on entries sets Graph.width(cr.width).height(cr.height) to keep the
canvas responsive as the sidebar opens/resizes.

Comment on lines +7 to +18
export function toggleContainer(container, onOpen) {
// Check if we're on mobile (768px breakpoint to match CSS)
const isMobile = window.innerWidth <= 768;

if (!DOM.menuContainer.classList.contains('open')) {
DOM.menuContainer.classList.add('open');
DOM.sideMenuButton.style.display = 'none';

// find all the containers with class "sidebar-container" and if open remove open
const allContainers = document.querySelectorAll('.sidebar-container');
allContainers.forEach((c) => {
if (c !== container && c.classList.contains('open')) {
c.classList.remove('open');
}
});

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against null/undefined container.

toggleContainer assumes a valid HTMLElement; if a caller passes null (e.g., missing markup), this will throw.

Apply this diff:

-export function toggleContainer(container, onOpen) {
+export function toggleContainer(container, onOpen) {
+    if (!container) {
+        console.warn('toggleContainer: no container provided');
+        return;
+    }
📝 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
export function toggleContainer(container, onOpen) {
// Check if we're on mobile (768px breakpoint to match CSS)
const isMobile = window.innerWidth <= 768;
if (!DOM.menuContainer.classList.contains('open')) {
DOM.menuContainer.classList.add('open');
DOM.sideMenuButton.style.display = 'none';
// find all the containers with class "sidebar-container" and if open remove open
const allContainers = document.querySelectorAll('.sidebar-container');
allContainers.forEach((c) => {
if (c !== container && c.classList.contains('open')) {
c.classList.remove('open');
}
});
export function toggleContainer(container, onOpen) {
if (!container) {
console.warn('toggleContainer: no container provided');
return;
}
// Check if we're on mobile (768px breakpoint to match CSS)
const isMobile = window.innerWidth <= 768;
// find all the containers with class "sidebar-container" and if open remove open
const allContainers = document.querySelectorAll('.sidebar-container');
allContainers.forEach((c) => {
if (c !== container && c.classList.contains('open')) {
c.classList.remove('open');
}
});
🤖 Prompt for AI Agents
In api/static/js/modules/ui.js around lines 7 to 18, toggleContainer assumes the
passed container is a valid HTMLElement and will throw if null/undefined; add an
early guard that checks the argument is non-null and is an Element/HTMLElement
before using it (e.g., if (!container || !(container instanceof Element))
return;), then proceed with the existing logic so callers that pass null or
missing markup no longer cause runtime errors.

Comment on lines 141 to 175
export function setupToolbar() {
// Keyboard navigation: roving tabindex within #toolbar-buttons
const toolbar = document.getElementById('toolbar-buttons');
if (toolbar) {
const buttons = Array.from(toolbar.querySelectorAll('button.toolbar-button'));
// Ensure first button is tabbable
buttons.forEach((b, i) => b.setAttribute('tabindex', i === 0 ? '0' : '-1'));

toolbar.addEventListener('keydown', (e) => {
const focused = document.activeElement;
const idx = buttons.indexOf(focused);
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
const next = buttons[(idx + 1) % buttons.length];
if (next) {
buttons.forEach(b => b.setAttribute('tabindex', '-1'));
next.setAttribute('tabindex', '0');
next.focus();
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
const prev = buttons[(idx - 1 + buttons.length) % buttons.length];
if (prev) {
buttons.forEach(b => b.setAttribute('tabindex', '-1'));
prev.setAttribute('tabindex', '0');
prev.focus();
}
} else if (e.key === 'Enter' || e.key === ' ') {
// Activate the button
e.preventDefault();
if (focused) focused.click();
}
});
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden toolbar keyboard navigation and use centralized DOM reference.

  • Use DOM.toolbarButtons to keep selectors centralized.
  • Add role="toolbar" and aria-orientation for a11y.
  • Handle empty toolbar, idx === -1, Home/End keys.
  • Keep roving tabindex in sync on click as well.

Apply this diff:

-export function setupToolbar() {
-    // Keyboard navigation: roving tabindex within #toolbar-buttons
-    const toolbar = document.getElementById('toolbar-buttons');
-    if (toolbar) {
-        const buttons = Array.from(toolbar.querySelectorAll('button.toolbar-button'));
-        // Ensure first button is tabbable
-        buttons.forEach((b, i) => b.setAttribute('tabindex', i === 0 ? '0' : '-1'));
-
-        toolbar.addEventListener('keydown', (e) => {
-            const focused = document.activeElement;
-            const idx = buttons.indexOf(focused);
-            if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
-                e.preventDefault();
-                const next = buttons[(idx + 1) % buttons.length];
-                if (next) {
-                    buttons.forEach(b => b.setAttribute('tabindex', '-1'));
-                    next.setAttribute('tabindex', '0');
-                    next.focus();
-                }
-            } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
-                e.preventDefault();
-                const prev = buttons[(idx - 1 + buttons.length) % buttons.length];
-                if (prev) {
-                    buttons.forEach(b => b.setAttribute('tabindex', '-1'));
-                    prev.setAttribute('tabindex', '0');
-                    prev.focus();
-                }
-            } else if (e.key === 'Enter' || e.key === ' ') {
-                // Activate the button
-                e.preventDefault();
-                if (focused) focused.click();
-            }
-        });
-    }
-}
+export function setupToolbar() {
+    // Keyboard navigation: roving tabindex within #toolbar-buttons
+    const toolbar = DOM.toolbarButtons || document.getElementById('toolbar-buttons');
+    if (!toolbar) return;
+    const buttons = Array.from(toolbar.querySelectorAll('button.toolbar-button'));
+    if (buttons.length === 0) return;
+
+    toolbar.setAttribute('role', 'toolbar');
+    toolbar.setAttribute('aria-orientation', 'vertical');
+
+    const setActive = (el) => {
+        buttons.forEach(b => b.setAttribute('tabindex', '-1'));
+        if (el) el.setAttribute('tabindex', '0');
+    };
+    // Ensure first button is tabbable
+    setActive(buttons[0]);
+
+    // Keep roving tabindex in sync on click
+    buttons.forEach((b) => {
+        b.addEventListener('click', () => setActive(b));
+    });
+
+    toolbar.addEventListener('keydown', (e) => {
+        const focused = document.activeElement;
+        let idx = buttons.indexOf(focused);
+        if (idx === -1) idx = 0;
+        if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
+            e.preventDefault();
+            const next = buttons[(idx + 1) % buttons.length];
+            if (next) {
+                setActive(next);
+                next.focus();
+            }
+        } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
+            e.preventDefault();
+            const prev = buttons[(idx - 1 + buttons.length) % buttons.length];
+            if (prev) {
+                setActive(prev);
+                prev.focus();
+            }
+        } else if (e.key === 'Home') {
+            e.preventDefault();
+            setActive(buttons[0]); buttons[0].focus();
+        } else if (e.key === 'End') {
+            e.preventDefault();
+            const last = buttons[buttons.length - 1];
+            setActive(last); last.focus();
+        } else if (e.key === 'Enter' || e.key === ' ') {
+            e.preventDefault();
+            if (focused && typeof focused.click === 'function') focused.click();
+        }
+    });
+}

Comment on lines +230 to +249
// Find all the containers with class "sidebar-container" and if open remove open
const allContainers = document.querySelectorAll('.sidebar-container');
allContainers.forEach((c) => {
// If menu is open and we switch to mobile, remove any desktop padding
if (isMobile && c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '';
DOM.chatContainer.style.paddingLeft = '';
}
// If menu is open and we switch to desktop, apply desktop padding
else if (!isMobile && c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '10%';
DOM.chatContainer.style.paddingLeft = '10%';
}
// If menu is closed and we're on desktop, ensure default desktop padding
else if (!isMobile && !c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '20%';
DOM.chatContainer.style.paddingLeft = '20%';
}
});

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

Fix padding logic on resize: current loop can override with closed containers.

Iterating each container and setting chat padding per container results in the last iteration “winning”, so an open container can be effectively ignored if any closed container is encountered later. Compute once based on “any open” instead.

Apply this diff:

-    // Find all the containers with class "sidebar-container" and if open remove open
-    const allContainers = document.querySelectorAll('.sidebar-container');
-    allContainers.forEach((c) => {
-        // If menu is open and we switch to mobile, remove any desktop padding
-        if (isMobile && c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '';
-            DOM.chatContainer.style.paddingLeft = '';
-        }
-        // If menu is open and we switch to desktop, apply desktop padding
-        else if (!isMobile && c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '10%';
-            DOM.chatContainer.style.paddingLeft = '10%';
-        }
-        // If menu is closed and we're on desktop, ensure default desktop padding
-        else if (!isMobile && !c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '20%';
-            DOM.chatContainer.style.paddingLeft = '20%';
-        }
-    });
+    const allContainers = document.querySelectorAll('.sidebar-container');
+    const anyOpen = Array.from(allContainers).some(c => c.classList.contains('open'));
+
+    if (isMobile) {
+        DOM.chatContainer.style.paddingRight = '';
+        DOM.chatContainer.style.paddingLeft = '';
+        return;
+    }
+    if (anyOpen) {
+        DOM.chatContainer.style.paddingRight = '10%';
+        DOM.chatContainer.style.paddingLeft = '10%';
+    } else {
+        DOM.chatContainer.style.paddingRight = '20%';
+        DOM.chatContainer.style.paddingLeft = '20%';
+    }
📝 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
// Find all the containers with class "sidebar-container" and if open remove open
const allContainers = document.querySelectorAll('.sidebar-container');
allContainers.forEach((c) => {
// If menu is open and we switch to mobile, remove any desktop padding
if (isMobile && c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '';
DOM.chatContainer.style.paddingLeft = '';
}
// If menu is open and we switch to desktop, apply desktop padding
else if (!isMobile && c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '10%';
DOM.chatContainer.style.paddingLeft = '10%';
}
// If menu is closed and we're on desktop, ensure default desktop padding
else if (!isMobile && !c.classList.contains('open')) {
DOM.chatContainer.style.paddingRight = '20%';
DOM.chatContainer.style.paddingLeft = '20%';
}
});
const allContainers = document.querySelectorAll('.sidebar-container');
const anyOpen = Array.from(allContainers).some(c => c.classList.contains('open'));
if (isMobile) {
DOM.chatContainer.style.paddingRight = '';
DOM.chatContainer.style.paddingLeft = '';
return;
}
if (anyOpen) {
DOM.chatContainer.style.paddingRight = '10%';
DOM.chatContainer.style.paddingLeft = '10%';
} else {
DOM.chatContainer.style.paddingRight = '20%';
DOM.chatContainer.style.paddingLeft = '20%';
}
🤖 Prompt for AI Agents
In api/static/js/modules/ui.js around lines 230 to 249, the current loop updates
DOM.chatContainer padding for every sidebar-container so later iterations can
override earlier ones; instead determine whether any container is open once
(e.g., Array.from(document.querySelectorAll('.sidebar-container')).some(c =>
c.classList.contains('open'))) and then set padding once based on isMobile and
that single boolean: clear padding when mobile, apply 10% when desktop and any
open, and apply 20% when desktop and none open.

@gkorland gkorland linked an issue Aug 20, 2025 that may be closed by this pull request
@gkorland gkorland changed the title add left toolbar add left toolbar and show the graph schema Aug 20, 2025
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: 2

♻️ Duplicate comments (3)
api/static/js/modules/schema.js (3)

17-19: Guard against non-array columns in rendering

Even if the backend normally supplies an array, guard client-side to prevent runtime errors.

Apply this diff:

-            // compute box height from number of columns
-            const boxHeight = headerHeight + node.columns.length * lineHeight + padding * 2;
+            // compute box height from number of columns (defensive)
+            const columns = Array.isArray(node.columns) ? node.columns : [];
+            const boxHeight = headerHeight + columns.length * lineHeight + padding * 2;
@@
-                node.columns.forEach((col) => {
+                columns.forEach((col) => {

Also applies to: 39-75


132-166: Size the canvas to the sidebar container and keep it responsive

Sizing to the window causes overflow in the sidebar; read container bounds and observe resizes.

Apply this diff:

-    Graph.onEngineStop(() => {
-        console.log('Engine stopped, layout stabilized.');
-    }).width(window.innerWidth).height(window.innerHeight);
+    const el = document.getElementById('schema-graph');
+    const { width, height } = el.getBoundingClientRect();
+    Graph.onEngineStop(() => {
+        // layout stabilized
+    }).width(width).height(height);
+
+    // keep graph sized to container
+    try {
+        const ro = new ResizeObserver(entries => {
+            for (const entry of entries) {
+                const cr = entry.contentRect;
+                Graph.width(cr.width).height(cr.height);
+            }
+        });
+        ro.observe(el);
+    } catch (e) {
+        // ResizeObserver not available; ignore
+    }
@@
 }

78-113: Prevent early-tick crashes when link endpoints aren’t resolved yet

ForceGraph can pass links whose source/target haven’t been resolved to positioned node objects; accessing x/y can throw.

Apply this diff:

             const edgeColor = getEdgeColor();
             ctx.strokeStyle = edgeColor;
             ctx.lineWidth = 1.5;
-            ctx.beginPath();
-            ctx.moveTo(link.source.x, link.source.y);
-            ctx.lineTo(link.target.x, link.target.y);
-            ctx.stroke();
+            if (!link.source || !link.target || link.source.x == null || link.target.x == null) return;
+            ctx.beginPath();
+            ctx.moveTo(link.source.x, link.source.y);
+            ctx.lineTo(link.target.x, link.target.y);
+            ctx.stroke();
🧹 Nitpick comments (4)
api/routes/graphs.py (1)

71-74: Docstring mismatch: you return “links”, not “edges”

The docstring says edges, but the response uses links. Align wording to avoid confusion.

Apply this diff:

-    This endpoint returns a JSON object with two keys: `nodes` and `edges`.
-    Nodes contain a minimal set of properties (id, name, labels, props).
-    Edges contain source and target node names (or internal ids), type and props.
+    This endpoint returns a JSON object with two keys: `nodes` and `links`.
+    Nodes contain a minimal set of properties (id, name, columns).
+    Links contain source and target table names.
api/static/js/modules/schema.js (2)

4-6: Be explicit about node identity to future-proof link resolution

ForceGraph defaults to id='id', but make it explicit to avoid regressions if data shape changes.

Apply this diff:

-    const Graph = ForceGraph()(document.getElementById('schema-graph'))
-        .graphData(data)
+    const Graph = ForceGraph()(document.getElementById('schema-graph'))
+        .nodeId('id')
+        .graphData(data)

166-166: Return the Graph instance for callers who want to customize or teardown

Returning the instance makes testing and future composition easier.

Apply this diff:

-}
+    return Graph;
+}
api/static/js/app.js (1)

22-49: Consider aborting stale fetches to avoid race conditions on rapid switches

If users flip graphs quickly, earlier responses can race and overwrite newer renders. Abort previous requests.

Apply this diff to the function:

 async function loadAndShowGraph(selected) {
     if (!selected) {
         return;
     }
     try {
-        const resp = await fetch(`/graphs/${encodeURIComponent(selected)}/data`);
+        // Abort any in-flight schema fetch
+        if (window.__schemaFetchCtl) {
+            try { window.__schemaFetchCtl.abort(); } catch (e) {}
+        }
+        const ctl = new AbortController();
+        window.__schemaFetchCtl = ctl;
+        const resp = await fetch(`/graphs/${encodeURIComponent(selected)}/data`, { signal: ctl.signal });

Optionally, declare a module-scoped controller instead of using window:

let currentSchemaFetchController;

and replace window.__schemaFetchCtl with currentSchemaFetchController.


</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

**💡 Knowledge Base configuration:**

- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4fc937007242f8cf46cfb24609a7d8a145dffb24 and 06e15c98c3d05ffcd9a171ee1568cdf11f6af5a3.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `api/routes/graphs.py` (1 hunks)
* `api/static/js/app.js` (4 hunks)
* `api/static/js/modules/schema.js` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (3)</summary>

<details>
<summary>api/static/**</summary>


**📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)**

> Store frontend static assets under api/static/

Files:
- `api/static/js/modules/schema.js`
- `api/static/js/app.js`

</details>
<details>
<summary>api/routes/graphs.py</summary>


**📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)**

> Keep graph/database route handlers in api/routes/graphs.py

Files:
- `api/routes/graphs.py`

</details>
<details>
<summary>api/routes/**/*.py</summary>


**📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)**

> Place all additional Flask route handlers under api/routes/

Files:
- `api/routes/graphs.py`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>api/static/js/modules/schema.js (1)</summary><blockquote>

<details>
<summary>api/static/js/app.js (1)</summary>

* `data` (34-34)

</details>

</blockquote></details>
<details>
<summary>api/routes/graphs.py (1)</summary><blockquote>

<details>
<summary>api/auth/user_management.py (1)</summary>

* `token_required` (263-285)

</details>

</blockquote></details>
<details>
<summary>api/static/js/app.js (4)</summary><blockquote>

<details>
<summary>api/static/js/modules/schema.js (1)</summary>

* `showGraph` (2-166)

</details>
<details>
<summary>api/static/js/modules/config.js (2)</summary>

* `DOM` (39-65)
* `DOM` (39-65)

</details>
<details>
<summary>api/static/js/modules/ui.js (2)</summary>

* `toggleContainer` (7-39)
* `setupToolbar` (141-225)

</details>
<details>
<summary>api/static/js/modules/graphs.js (1)</summary>

* `onGraphChange` (115-117)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: CodeQL</summary>

<details>
<summary>api/routes/graphs.py</summary>

[failure] 84-84: Log Injection
This log entry depends on a [user-provided value](1).

---

[failure] 105-105: Log Injection
This log entry depends on a [user-provided value](1).

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Pylint</summary>

<details>
<summary>api/routes/graphs.py</summary>

[warning] 40-40: Pylint: Unnecessary 'elif' after 'return'. (no-else-return).

---

[warning] 68-68: Pylint: Too many local variables (21/15).

---

[warning] 83-83: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 104-104: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 112-112: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 140-140: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 154-154: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 68-68: Pylint: Too many branches (16/12).

---

[warning] 68-68: Pylint: Too many statements (52/50).

---

[warning] 167-167: Pylint: Too many return statements (8/6).

---

[warning] 278-278: Pylint: Too many local variables (28/15).

---

[warning] 324-324: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 478-478: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 278-278: Pylint: Too many branches (18/12).

---

[warning] 278-278: Pylint: Too many statements (78/50).

---

[warning] 289-289: Pylint: Unused variable 'db_type' (unused-variable).

---

[warning] 246-246: Pylint: Too many statements (90/50).

---

[warning] 586-586: Pylint: Broad exception caught. (broad-exception-caught).

---

[warning] 509-509: Pylint: Unused variable 'db_type' (unused-variable).

---

[warning] 647-647: Pylint: Broad exception caught. (broad-exception-caught).

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>api/routes/graphs.py (3)</summary>

`66-74`: **Nice addition: user-scoped, authenticated graph-data endpoint**

Route shape, auth, and namespacing look good and align with the frontend fetch.

---

`108-148`: **Robust column normalization is solid**

Tolerant of mixed driver return shapes and safely skips malformed entries. Good defensive coding.

---

`149-162`: **Link deduplication and shape look good**

Dedup via set and return {source, target} matches frontend expectations.

</details>
<details>
<summary>api/static/js/app.js (2)</summary>

`76-91`: **Good UX: fetch on panel open and size the work to current selection**

Wires schema panel open to data load correctly and avoids unnecessary fetches when nothing is selected.

---

`112-124`: **Nice: refresh graph view when the selection changes while panel is open**

Preserves existing behavior and adds the right conditional fetch.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines 101 to 107
try:
tables_res = graph.query(tables_query).result_set
links_res = graph.query(links_query).result_set
except Exception as e:
logging.error("Error querying graph data for %s: %s", namespaced, e)
return jsonify({"error": "Failed to read graph data"}), 500

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

Sanitize log entry and keep stack trace (CodeQL: Log Injection)

Use %r for the user-controlled name and include exc_info for debugging.

Apply this diff:

-    except Exception as e:
-        logging.error("Error querying graph data for %s: %s", namespaced, e)
+    except Exception as e:
+        logging.error("Error querying graph data for %r: %s", namespaced, e, exc_info=True)
         return jsonify({"error": "Failed to read graph data"}), 500
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 105-105: Log Injection
This log entry depends on a user-provided value.

🪛 GitHub Actions: Pylint

[warning] 104-104: Pylint: Broad exception caught. (broad-exception-caught).

🤖 Prompt for AI Agents
In api/routes/graphs.py around lines 101 to 107, the logging.error call logs a
user-controlled value directly and omits the exception stack trace; change the
log to use a safe representation (%r) for the namespaced value and pass
exc_info=True to include the exception traceback (e.g., logging.error("Error
querying graph data for %r", namespaced, exc_info=True, stack_info=False)) so
the message is sanitized and full error context is retained; keep the existing
JSON error response unchanged.

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

♻️ Duplicate comments (5)
api/routes/graphs.py (1)

84-85: Fix CodeQL "Log Injection" finding: sanitize user-controlled data in logs

Format with %r to escape control characters and prevent log forging. This was flagged earlier and remains unfixed.

-        logging.error("Failed to select graph %s: %s", namespaced, e)
+        logging.error("Failed to select graph %r: %r", namespaced, e)
@@
-        logging.error("Error querying graph data for %s: %s", namespaced, e)
+        logging.error("Error querying graph data for %r: %r", namespaced, e)

Also applies to: 105-106

api/static/js/modules/schema.js (4)

17-19: Defensive rendering: guard when node.columns is missing or not an array

Avoids runtime errors if data is malformed or in future changes. This was raised previously.

-            // compute box height from number of columns
-            const boxHeight = headerHeight + node.columns.length * lineHeight + padding * 2;
+            // compute box height from number of columns (defensive)
+            const columns = Array.isArray(node.columns) ? node.columns : [];
+            const boxHeight = headerHeight + columns.length * lineHeight + padding * 2;

39-75: Use guarded columns array for iteration and width calculations

Follow-up to the guard above; iterate over the safe columns array to avoid exceptions.

-                node.columns.forEach((col) => {
+                columns.forEach((col) => {
                     let name = col;
                     let type = null;
                     if (typeof col === 'object') {
                         name = col.name || '';
                         type = col.type || col.dataType || null;
                     }
@@
-                        const nameWidth = ctx.measureText(name).width;
+                        const nameWidth = ctx.measureText(name).width;

132-136: Size canvas to container instead of viewport; add resize observer

The schema graph lives in a sidebar; sizing to window causes overflow and layout issues. Use the container box. Previously suggested.

-    Graph.onEngineStop(() => {
-        console.log('Engine stopped, layout stabilized.');
-    }).width(window.innerWidth).height(window.innerHeight);
+    const el = document.getElementById('schema-graph');
+    const { width, height } = el.getBoundingClientRect();
+    Graph.onEngineStop(() => {
+        // layout stabilized
+    }).width(width).height(height);

Add this right after the block above to keep it responsive:

// keep graph sized to container
const ro = new ResizeObserver(entries => {
  for (const entry of entries) {
    const cr = entry.contentRect;
    Graph.width(cr.width).height(cr.height);
  }
});
ro.observe(document.getElementById('schema-graph'));

78-113: Prevent early-tick crashes when link positions are undefined; improve robustness

During early ticks link.source/target may be unresolved or lack x/y. Guard before drawing. This was suggested earlier.

-        .linkCanvasObject((link, ctx) => {
+        .linkCanvasObject((link, ctx) => {
             const getEdgeColor = () => {
@@
             const edgeColor = getEdgeColor();
             ctx.strokeStyle = edgeColor;
             ctx.lineWidth = 1.5;
             ctx.beginPath();
-            ctx.moveTo(link.source.x, link.source.y);
-            ctx.lineTo(link.target.x, link.target.y);
+            if (!link?.source || !link?.target ||
+                link.source.x == null || link.source.y == null ||
+                link.target.x == null || link.target.y == null) {
+                return;
+            }
+            ctx.moveTo(link.source.x, link.source.y);
+            ctx.lineTo(link.target.x, link.target.y);
             ctx.stroke();
         })
🧹 Nitpick comments (7)
api/routes/graphs.py (2)

71-74: Docstring mismatch: returns "links", not "edges"

The API returns {"nodes": [...], "links": [...]}, but the docstring says "edges". Align to avoid confusion and to match the frontend expectations.

-    This endpoint returns a JSON object with two keys: `nodes` and `edges`.
-    Nodes contain a minimal set of properties (id, name, labels, props).
-    Edges contain source and target node names (or internal ids), type and props.
+    This endpoint returns a JSON object with two keys: `nodes` and `links`.
+    Nodes contain: id, name, and columns (list of {name, type|null}).
+    Links contain: source and target table names (deduplicated).

75-80: Harden graph_id and keep length limits consistent with other routes

  • Align the max length with query_graph (which trims to 100 chars at Line 255).
  • Add a conservative character whitelist to avoid surprising graph names and reduce risk of log forging or path traversal in downstream systems.
-    graph_id = graph_id.strip()[:200]
+    graph_id = graph_id.strip()[:100]
+    # allow only alphanumerics, underscore and hyphen
+    if not all(c.isalnum() or c in ('_', '-') for c in graph_id):
+        return jsonify({"error": "Invalid graph_id"}), 400
     namespaced = g.user_id + "_" + graph_id
api/static/js/modules/schema.js (1)

4-6: Set nodeId explicitly to 'id' to match link force mapping

You’re already using d3.forceLink().id(d => d.id). Make the Graph’s nodeId explicit and consistent to remove ambiguity and future regressions.

-    const Graph = ForceGraph()(document.getElementById('schema-graph'))
-        .graphData(data)
+    const Graph = ForceGraph()(document.getElementById('schema-graph'))
+        .nodeId('id')
+        .graphData(data)
api/static/js/app.js (4)

28-29: Minor: hint desired response type to the server

Not required, but explicitly requesting JSON is a harmless clarity/interop improvement.

-        const resp = await fetch(`/graphs/${encodeURIComponent(selected)}/data`);
+        const resp = await fetch(`/graphs/${encodeURIComponent(selected)}/data`, {
+            headers: { 'Accept': 'application/json' }
+        });

36-39: Clear schema container on invalid data to avoid stale graph view

If shape is invalid, clear the container so users don’t see an outdated graph.

-        if (!data || !Array.isArray(data.nodes) || !Array.isArray(data.links)) {
-            console.warn('Graph data returned in unexpected shape, showing empty message', data);
-            return;
-        }
+        if (!data || !Array.isArray(data.nodes) || !Array.isArray(data.links)) {
+            console.warn('Graph data returned in unexpected shape, clearing view', data);
+            const container = document.getElementById('schema-graph');
+            if (container) container.innerHTML = '';
+            return;
+        }

46-49: Also clear schema container when fetch fails

Keeps UI consistent when network/parse errors occur.

     } catch (err) {
         console.error('Error fetching graph data:', err);
+        const container = document.getElementById('schema-graph');
+        if (container) container.innerHTML = '';
     }

112-124: When selection is cleared and schema panel is open, clear the graph

Prevents showing a graph from a previously selected database when no selection is active.

     DOM.graphSelect.addEventListener("change", async () => {
         // Preserve existing behavior
         onGraphChange();

         // If the schema panel is currently open, fetch and render the selected graph
         const selected = DOM.graphSelect.value;
-        if (!selected) return;
-
-        if (DOM.schemaContainer && DOM.schemaContainer.classList.contains('open')) {
-            await loadAndShowGraph(selected);
-        }
+        if (DOM.schemaContainer && DOM.schemaContainer.classList.contains('open')) {
+            if (!selected) {
+                const container = document.getElementById('schema-graph');
+                if (container) container.innerHTML = '';
+                return;
+            }
+            await loadAndShowGraph(selected);
+        }
     });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4fc9370 and 06e15c9.

📒 Files selected for processing (3)
  • api/routes/graphs.py (1 hunks)
  • api/static/js/app.js (4 hunks)
  • api/static/js/modules/schema.js (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
api/routes/graphs.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Keep graph/database route handlers in api/routes/graphs.py

Files:

  • api/routes/graphs.py
api/routes/**/*.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Place all additional Flask route handlers under api/routes/

Files:

  • api/routes/graphs.py
api/static/**

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store frontend static assets under api/static/

Files:

  • api/static/js/modules/schema.js
  • api/static/js/app.js
🧬 Code Graph Analysis (3)
api/routes/graphs.py (1)
api/auth/user_management.py (1)
  • token_required (263-285)
api/static/js/modules/schema.js (1)
api/static/js/app.js (1)
  • data (34-34)
api/static/js/app.js (4)
api/static/js/modules/schema.js (1)
  • showGraph (2-166)
api/static/js/modules/config.js (2)
  • DOM (39-65)
  • DOM (39-65)
api/static/js/modules/ui.js (2)
  • toggleContainer (7-39)
  • setupToolbar (141-225)
api/static/js/modules/graphs.js (1)
  • onGraphChange (115-117)
🪛 GitHub Check: CodeQL
api/routes/graphs.py

[failure] 84-84: Log Injection
This log entry depends on a user-provided value.


[failure] 105-105: Log Injection
This log entry depends on a user-provided value.

Comment on lines 81 to 86
try:
graph = db.select_graph(namespaced)
except Exception as e:
logging.error("Failed to select graph %s: %s", namespaced, e)
return jsonify({"error": "Graph not found or database error"}), 404

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle missing graph object explicitly after selection

If db.select_graph returns a falsy object instead of throwing, we should respond with 404. This avoids attribute errors when accessing graph.query.

     try:
         graph = db.select_graph(namespaced)
     except Exception as e:
         logging.error("Failed to select graph %s: %s", namespaced, e)
         return jsonify({"error": "Graph not found or database error"}), 404
+    if not graph:
+        return jsonify({"error": "Graph not found"}), 404
📝 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
try:
graph = db.select_graph(namespaced)
except Exception as e:
logging.error("Failed to select graph %s: %s", namespaced, e)
return jsonify({"error": "Graph not found or database error"}), 404
try:
graph = db.select_graph(namespaced)
except Exception as e:
logging.error("Failed to select graph %s: %s", namespaced, e)
return jsonify({"error": "Graph not found or database error"}), 404
if not graph:
return jsonify({"error": "Graph not found"}), 404
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 84-84: Log Injection
This log entry depends on a user-provided value.

🤖 Prompt for AI Agents
In api/routes/graphs.py around lines 81 to 86, the code only handles exceptions
from db.select_graph but not the case where it returns a falsy value; add an
explicit check after the try/except: if graph is None or falsy, log an
informative message including the namespaced id and return a 404 JSON response
so subsequent access like graph.query won't raise an AttributeError.

try:
graph = db.select_graph(namespaced)
except Exception as e:
logging.error("Failed to select graph %s: %s", sanitize_log_input(namespaced), e)

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fix the log injection vulnerability, we should improve the sanitize_log_input function to remove or escape all control characters that could affect log formatting, not just newlines and carriage returns. This includes tab (\t), form feed (\f), vertical tab (\v), and Unicode line/paragraph separators. The function should also clearly mark user input in the log entry, for example by quoting it. The best way to do this is to use a regular expression to remove all characters in the Unicode "control" category, or to replace them with safe alternatives (such as spaces). The change should be made in the definition of sanitize_log_input in api/routes/graphs.py. No changes to the logging calls themselves are needed if the sanitization is robust.

Required changes:

  • Update the sanitize_log_input function to remove all control characters using a regular expression.
  • Add the required import for re (the regular expression module).

Suggested changeset 1
api/routes/graphs.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/routes/graphs.py b/api/routes/graphs.py
--- a/api/routes/graphs.py
+++ b/api/routes/graphs.py
@@ -2,6 +2,7 @@
 
 import json
 import logging
+import re
 from concurrent.futures import ThreadPoolExecutor
 from concurrent.futures import TimeoutError as FuturesTimeoutError
 
@@ -50,10 +51,13 @@
     return query.replace('\n', ' ').replace('\r', ' ')[:500]
 
 def sanitize_log_input(value: str) -> str:
-    """Sanitize input for safe logging (remove newlines and carriage returns)."""
+    """Sanitize input for safe logging (remove all control characters and clearly mark user input)."""
     if not isinstance(value, str):
-        return str(value)
-    return value.replace('\n', ' ').replace('\r', ' ')
+        value = str(value)
+    # Remove all Unicode control characters (category Cc)
+    value = re.sub(r'[\x00-\x1F\x7F-\x9F]', ' ', value)
+    # Optionally, quote the user input to make it clear in logs
+    return f'"{value}"'
 
 @graphs_bp.route("")
 @token_required
EOF
@@ -2,6 +2,7 @@

import json
import logging
import re
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError as FuturesTimeoutError

@@ -50,10 +51,13 @@
return query.replace('\n', ' ').replace('\r', ' ')[:500]

def sanitize_log_input(value: str) -> str:
"""Sanitize input for safe logging (remove newlines and carriage returns)."""
"""Sanitize input for safe logging (remove all control characters and clearly mark user input)."""
if not isinstance(value, str):
return str(value)
return value.replace('\n', ' ').replace('\r', ' ')
value = str(value)
# Remove all Unicode control characters (category Cc)
value = re.sub(r'[\x00-\x1F\x7F-\x9F]', ' ', value)
# Optionally, quote the user input to make it clear in logs
return f'"{value}"'

@graphs_bp.route("")
@token_required
Copilot is powered by AI and may make mistakes. Always verify output.
tables_res = graph.query(tables_query).result_set
links_res = graph.query(links_query).result_set
except Exception as e:
logging.error("Error querying graph data for %s: %s", sanitize_log_input(namespaced), e)

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fix the log injection vulnerability, we should enhance the sanitize_log_input function to remove all control characters (not just newlines and carriage returns) from user input before logging. This can be done by using a regular expression to remove all characters with ASCII codes below 0x20 (except for space), which includes tabs and other control characters. Additionally, we should clearly mark user input in the log entry to prevent confusion. The changes should be made in the sanitize_log_input function in api/routes/graphs.py, and all calls to this function will benefit from the improved sanitization.

Suggested changeset 1
api/routes/graphs.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/routes/graphs.py b/api/routes/graphs.py
--- a/api/routes/graphs.py
+++ b/api/routes/graphs.py
@@ -49,11 +49,14 @@
     """Sanitize the query to prevent injection attacks."""
     return query.replace('\n', ' ').replace('\r', ' ')[:500]
 
+import re
+
 def sanitize_log_input(value: str) -> str:
-    """Sanitize input for safe logging (remove newlines and carriage returns)."""
+    """Sanitize input for safe logging (remove control characters)."""
     if not isinstance(value, str):
-        return str(value)
-    return value.replace('\n', ' ').replace('\r', ' ')
+        value = str(value)
+    # Remove all ASCII control characters except space (0x20)
+    return re.sub(r'[\x00-\x1F\x7F]', '', value)
 
 @graphs_bp.route("")
 @token_required
EOF
@@ -49,11 +49,14 @@
"""Sanitize the query to prevent injection attacks."""
return query.replace('\n', ' ').replace('\r', ' ')[:500]

import re

def sanitize_log_input(value: str) -> str:
"""Sanitize input for safe logging (remove newlines and carriage returns)."""
"""Sanitize input for safe logging (remove control characters)."""
if not isinstance(value, str):
return str(value)
return value.replace('\n', ' ').replace('\r', ' ')
value = str(value)
# Remove all ASCII control characters except space (0x20)
return re.sub(r'[\x00-\x1F\x7F]', '', value)

@graphs_bp.route("")
@token_required
Copilot is powered by AI and may make mistakes. Always verify output.
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: 2

♻️ Duplicate comments (8)
api/static/css/menu.css (1)

166-179: Schema graph selector mismatch fixed; consider a temporary fallback to #graph

Good catch switching the container to #schema-graph and ensuring the canvas fills it. If any legacy templates still reference id="graph", consider including both selectors for a short transition period to avoid unexpected regressions.

-#schema-graph {
+#schema-graph,
+#graph {
   width: 100%;
   height: 60vh; /* allow graph to take most of the sidebar height */
   min-height: 300px;
   background: transparent;
   position: relative;
 }
 
- .force-graph-container, #schema-graph > canvas {
+ .force-graph-container,
+ #schema-graph > canvas,
+ #graph > canvas {
   width: 100% !important;
   height: 100% !important;
 }

You can verify template usage with:

#!/bin/bash
rg -nC2 'id="schema-graph"|id="graph"' -g 'api/templates/**' -S
api/static/js/modules/ui.js (3)

141-175: Harden toolbar keyboard navigation and improve a11y semantics

Add role/aria attributes, handle empty toolbars, Home/End keys, and keep roving tabindex in sync on click. This improves accessibility and robustness.

-export function setupToolbar() {
-    // Keyboard navigation: roving tabindex within #toolbar-buttons
-    const toolbar = document.getElementById('toolbar-buttons');
-    if (toolbar) {
-        const buttons = Array.from(toolbar.querySelectorAll('button.toolbar-button'));
-        // Ensure first button is tabbable
-        buttons.forEach((b, i) => b.setAttribute('tabindex', i === 0 ? '0' : '-1'));
-
-        toolbar.addEventListener('keydown', (e) => {
-            const focused = document.activeElement;
-            const idx = buttons.indexOf(focused);
-            if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
-                e.preventDefault();
-                const next = buttons[(idx + 1) % buttons.length];
-                if (next) {
-                    buttons.forEach(b => b.setAttribute('tabindex', '-1'));
-                    next.setAttribute('tabindex', '0');
-                    next.focus();
-                }
-            } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
-                e.preventDefault();
-                const prev = buttons[(idx - 1 + buttons.length) % buttons.length];
-                if (prev) {
-                    buttons.forEach(b => b.setAttribute('tabindex', '-1'));
-                    prev.setAttribute('tabindex', '0');
-                    prev.focus();
-                }
-            } else if (e.key === 'Enter' || e.key === ' ') {
-                // Activate the button
-                e.preventDefault();
-                if (focused) focused.click();
-            }
-        });
-    }
-}
+export function setupToolbar() {
+    const toolbar = (typeof DOM !== 'undefined' && DOM.toolbarButtons) || document.getElementById('toolbar-buttons');
+    if (!toolbar) return;
+    const buttons = Array.from(toolbar.querySelectorAll('button.toolbar-button'));
+    if (buttons.length === 0) return;
+
+    toolbar.setAttribute('role', 'toolbar');
+    toolbar.setAttribute('aria-orientation', 'vertical');
+
+    const setActive = (el) => {
+        buttons.forEach(b => b.setAttribute('tabindex', '-1'));
+        if (el) el.setAttribute('tabindex', '0');
+    };
+    setActive(buttons[0]);
+
+    buttons.forEach((b) => b.addEventListener('click', () => setActive(b)));
+
+    toolbar.addEventListener('keydown', (e) => {
+        const focused = document.activeElement;
+        let idx = buttons.indexOf(focused);
+        if (idx === -1) idx = 0;
+        if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
+            e.preventDefault();
+            const next = buttons[(idx + 1) % buttons.length];
+            setActive(next); next.focus();
+        } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
+            e.preventDefault();
+            const prev = buttons[(idx - 1 + buttons.length) % buttons.length];
+            setActive(prev); prev.focus();
+        } else if (e.key === 'Home') {
+            e.preventDefault();
+            setActive(buttons[0]); buttons[0].focus();
+        } else if (e.key === 'End') {
+            e.preventDefault();
+            const last = buttons[buttons.length - 1];
+            setActive(last); last.focus();
+        } else if (e.key === 'Enter' || e.key === ' ') {
+            e.preventDefault();
+            if (focused && typeof focused.click === 'function') focused.click();
+        }
+    });
+}

7-17: Guard against null/invalid container to avoid runtime errors

toggleContainer assumes a valid element. Add a defensive guard so missing markup or bad calls don’t throw.

-export function toggleContainer(container, onOpen) {
+export function toggleContainer(container, onOpen) {
+    if (!container || !(container instanceof Element)) {
+        console.warn('toggleContainer: no valid container provided');
+        return;
+    }

177-201: Fix chat padding logic on resize: current loop can override with closed containers

Set padding once based on whether any sidebar is open, instead of per-container. Prevents later iterations from overriding an open state.

-    // Find all the containers with class "sidebar-container" and if open remove open
-    const allContainers = document.querySelectorAll('.sidebar-container');
-    allContainers.forEach((c) => {
-        // If menu is open and we switch to mobile, remove any desktop padding
-        if (isMobile && c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '';
-            DOM.chatContainer.style.paddingLeft = '';
-        }
-        // If menu is open and we switch to desktop, apply desktop padding
-        else if (!isMobile && c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '10%';
-            DOM.chatContainer.style.paddingLeft = '10%';
-        }
-        // If menu is closed and we're on desktop, ensure default desktop padding
-        else if (!isMobile && !c.classList.contains('open')) {
-            DOM.chatContainer.style.paddingRight = '20%';
-            DOM.chatContainer.style.paddingLeft = '20%';
-        }
-    });
+    const allContainers = document.querySelectorAll('.sidebar-container');
+    const anyOpen = Array.from(allContainers).some(c => c.classList.contains('open'));
+    if (isMobile) {
+        DOM.chatContainer.style.paddingRight = '';
+        DOM.chatContainer.style.paddingLeft = '';
+        return;
+    }
+    if (anyOpen) {
+        DOM.chatContainer.style.paddingRight = '10%';
+        DOM.chatContainer.style.paddingLeft = '10%';
+    } else {
+        DOM.chatContainer.style.paddingRight = '20%';
+        DOM.chatContainer.style.paddingLeft = '20%';
+    }
api/routes/graphs.py (1)

107-113: Include traceback and safe formatting when query fails

Use %r for the namespaced id and include exc_info for better diagnostics. This also addresses the CodeQL log-injection alert.

-    except Exception as e:
-        logging.error("Error querying graph data for %s: %s", sanitize_log_input(namespaced), e)
-        return jsonify({"error": "Failed to read graph data"}), 500
+    except Exception as e:
+        logging.error("Error querying graph data for %r: %s", namespaced, e, exc_info=True)
+        return jsonify({"error": "Failed to read graph data"}), 500
api/static/js/modules/schema.js (3)

18-20: Defensive rendering for columns to prevent crashes when data is missing

node.columns is assumed to be an array; if it’s missing or not an array, this will throw. Default to an empty array for safe rendering.

-            // compute box height from number of columns
-            const boxHeight = headerHeight + node.columns.length * lineHeight + padding * 2;
+            // compute box height from number of columns (defensive)
+            const columns = Array.isArray(node.columns) ? node.columns : [];
+            const boxHeight = headerHeight + columns.length * lineHeight + padding * 2;
@@
-                node.columns.forEach((col) => {
+                columns.forEach((col) => {

Also applies to: 40-76


79-114: Guard link coordinates during early ticks to avoid canvas errors and improve contrast

link.source/target may be objects without x/y during early ticks. Add a guard to skip until positions exist. Consider a neutral gray for better contrast on light themes; your dynamic color logic is good, but still guard coordinates.

-        .linkCanvasObject((link, ctx) => {
+        .linkCanvasObject((link, ctx) => {
+            if (!link?.source || !link?.target ||
+                link.source.x == null || link.source.y == null ||
+                link.target.x == null || link.target.y == null) {
+                return;
+            }
             const getEdgeColor = () => {
                 try {
                     const root = getComputedStyle(document.documentElement);
                     // Prefer explicit CSS variable if present
                     const cssEdge = root.getPropertyValue('--edge-color');
                     if (cssEdge && cssEdge.trim()) return cssEdge.trim();
@@
-                return '#ffffff';
+                return '#999';
             };
 
             const edgeColor = getEdgeColor();
             ctx.strokeStyle = edgeColor;
             ctx.lineWidth = 1.5;
             ctx.beginPath();
             ctx.moveTo(link.source.x, link.source.y);
             ctx.lineTo(link.target.x, link.target.y);
             ctx.stroke();
         })

133-137: Size the canvas to the container, not the window, and keep it responsive

Sizing to window spills beyond the sidebar panel. Use the container’s bounding box and a ResizeObserver to keep in sync when the sidebar opens/resizes.

-    Graph.onEngineStop(() => {
-        console.log('Engine stopped, layout stabilized.');
-    }).width(window.innerWidth).height(window.innerHeight);
+    const bb = el.getBoundingClientRect();
+    Graph.onEngineStop(() => {
+        // layout stabilized
+    }).width(bb.width).height(bb.height);
+
+    // Keep graph sized to container
+    const ro = new ResizeObserver(entries => {
+        for (const entry of entries) {
+            const cr = entry.contentRect;
+            Graph.width(cr.width).height(cr.height);
+        }
+    });
+    ro.observe(el);
🧹 Nitpick comments (2)
api/static/css/menu.css (1)

3-17: Animate the left offset or use margin to avoid a visual jump when opening

.left: 48px is applied only in the open state and isn’t part of the transition list, so it “jumps” into place. Either add left to the transition and set a closed-state baseline (left: 0) or, alternatively, use margin-left which is already part of the box model and feels more natural with layout.

 .sidebar-container {
   width: 0;
   min-width: 0;
   overflow: hidden;
   padding: 20px 0;
   display: flex;
   flex-direction: column;
   gap: 20px;
   background: var(--falkor-quaternary);
-  transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1), padding 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1),
+              padding 0.4s cubic-bezier(0.4, 0, 0.2, 1),
+              left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
   z-index: 1;
   position: relative;
+  left: 0; /* baseline for smooth transition */
   visibility: hidden;
   pointer-events: none;
 }
 
 .sidebar-container.open {
   width: 30dvw;
   padding: 20px;
   left: 48px;
   visibility: visible;
   pointer-events: auto;
 }

Also applies to: 19-25

api/routes/graphs.py (1)

77-80: Docstring/property mismatch: returns "links", not "edges"

The docstring mentions edges, but the response payload uses links. Align to avoid confusion for API consumers.

-    This endpoint returns a JSON object with two keys: `nodes` and `edges`.
+    This endpoint returns a JSON object with two keys: `nodes` and `links`.
-    Edges contain source and target node names (or internal ids), type and props.
+    Links contain source and target table names and optional properties.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06e15c9 and a41abbb.

📒 Files selected for processing (5)
  • api/routes/graphs.py (2 hunks)
  • api/static/css/buttons.css (2 hunks)
  • api/static/css/menu.css (3 hunks)
  • api/static/js/modules/schema.js (1 hunks)
  • api/static/js/modules/ui.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/static/css/buttons.css
🧰 Additional context used
📓 Path-based instructions (3)
api/static/**

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store frontend static assets under api/static/

Files:

  • api/static/js/modules/schema.js
  • api/static/js/modules/ui.js
  • api/static/css/menu.css
api/routes/graphs.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Keep graph/database route handlers in api/routes/graphs.py

Files:

  • api/routes/graphs.py
api/routes/**/*.py

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Place all additional Flask route handlers under api/routes/

Files:

  • api/routes/graphs.py
🧬 Code Graph Analysis (3)
api/static/js/modules/schema.js (1)
api/static/js/app.js (1)
  • data (34-34)
api/routes/graphs.py (1)
api/auth/user_management.py (1)
  • token_required (263-285)
api/static/js/modules/ui.js (1)
api/static/js/modules/config.js (2)
  • DOM (39-65)
  • DOM (39-65)
🪛 GitHub Check: CodeQL
api/routes/graphs.py

[failure] 90-90: Log Injection
This log entry depends on a user-provided value.


[failure] 111-111: Log Injection
This log entry depends on a user-provided value.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: e2e-tests
  • GitHub Check: e2e-tests
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
api/static/js/modules/schema.js (1)

119-128: Link force id accessor correctly matches nodeId

Using d => d.name aligns with Graph.nodeId('name') and your data model. Good.

Comment on lines +87 to +92
try:
graph = db.select_graph(namespaced)
except Exception as e:
logging.error("Failed to select graph %s: %s", sanitize_log_input(namespaced), e)
return jsonify({"error": "Graph not found or database error"}), 404

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

Handle missing graph object and harden logging (avoid None deref + log forging finding)

  • db.select_graph can return None; calling graph.query would raise. Return 404 early.
  • Use %r and exc_info=True in logs to mitigate log injection findings and preserve stack traces.
  • Consider 500 for select_graph exceptions (DB errors) vs 404 for “not found”.
     try:
-        graph = db.select_graph(namespaced)
-    except Exception as e:
-        logging.error("Failed to select graph %s: %s", sanitize_log_input(namespaced), e)
-        return jsonify({"error": "Graph not found or database error"}), 404
+        graph = db.select_graph(namespaced)
+    except Exception as e:
+        logging.error("Failed to select graph %r: %s", namespaced, e, exc_info=True)
+        return jsonify({"error": "Database error selecting graph"}), 500
+    if not graph:
+        logging.info("Graph not found: %r", namespaced)
+        return jsonify({"error": "Graph not found"}), 404

Also applies to: 93-106

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 90-90: Log Injection
This log entry depends on a user-provided value.

🤖 Prompt for AI Agents
In api/routes/graphs.py around lines 87-92 (and similarly for 93-106),
db.select_graph may return None or raise an exception; update the flow to first
handle a successful return: if graph is None return jsonify({"error":"Graph not
found"}), 404; if select_graph raises an exception treat it as a DB error and
return a 500. Replace the current logging call with a safe log that uses %r for
the namespaced value and include exc_info=True (e.g., logging.error("Failed to
select graph %r", namespaced, exc_info=True)) so stack traces are preserved and
log forging is mitigated, and ensure no graph attributes are accessed before the
None check.

Comment on lines +4 to +7
const Graph = ForceGraph()(document.getElementById('schema-graph'))
.graphData(data)
.nodeId('name')
// Custom node renderer: draw table-like boxes with columns
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Null-guard schema container and set nodeId before graphData

  • Guard against a missing #schema-graph element to prevent runtime errors.
  • Set nodeId('name') before graphData for deterministic link resolution.
-export function showGraph(data) {
-
-    const Graph = ForceGraph()(document.getElementById('schema-graph'))
-        .graphData(data)
-        .nodeId('name')
+export function showGraph(data) {
+    const el = document.getElementById('schema-graph');
+    if (!el) {
+        console.warn('showGraph: #schema-graph not found');
+        return;
+    }
+    const Graph = ForceGraph()(el)
+        .nodeId('name')
+        .graphData(data)
📝 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 Graph = ForceGraph()(document.getElementById('schema-graph'))
.graphData(data)
.nodeId('name')
// Custom node renderer: draw table-like boxes with columns
export function showGraph(data) {
const el = document.getElementById('schema-graph');
if (!el) {
console.warn('showGraph: #schema-graph not found');
return;
}
const Graph = ForceGraph()(el)
.nodeId('name')
.graphData(data)
// Custom node renderer: draw table-like boxes with columns
🤖 Prompt for AI Agents
In api/static/js/modules/schema.js around lines 4 to 7, the code assumes
document.getElementById('schema-graph') exists and calls ForceGraph() directly
on it and calls nodeId after graphData; change to first query and null-guard the
container (if not found, return/do nothing) and then instantiate ForceGraph with
that container, call .nodeId('name') before .graphData(data) so node identifiers
are set prior to resolving links; ensure the function exits gracefully when the
element is missing.

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

Caution

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

⚠️ Outside diff range comments (1)
api/static/css/responsive.css (1)

15-18: Stale selector: update to new sidebar class or rule won’t fire on mobile.

Mobile overlay rule still targets #menu-container.open, but the container was renamed to .sidebar-container. As-is, chat won’t reset its margin when the sidebar opens on small screens.

Apply:

-  #menu-container.open~#chat-container {
+  .sidebar-container.open ~ #chat-container {
     margin-left: 0;
   }
🧹 Nitpick comments (2)
api/static/css/responsive.css (2)

22-28: Use dynamic viewport units for better mobile behavior.

100vh has known issues on iOS/Android due to browser chrome. Prefer 100dvh for correct full-height overlay.

   .sidebar-container {
     position: fixed;
     top: 0;
     left: 0;
-    height: 100vh;
+    height: 100dvh;
     z-index: 999;
   }

124-130: Repositioning GitHub button: verify overlap and consider responsive spacing.

right: 60px may collide with the user-profile button on narrow screens or with the new left toolbar changes. Consider responsive spacing via clamp or, better, place both in a flex container to avoid absolute magic numbers.

Option A (minimal CSS-only tweak):

   #github-link-btn {
     top: 15px;
-    right: 60px;
+    right: clamp(48px, 6vw, 80px);
     padding: 6px 8px;
     font-size: 12px;
     height: 40px;
   }

Option B (preferred): group top-right actions in a flex container and drop absolute positioning for intrinsic layout (can follow up with a concrete snippet if you share the container markup).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a41abbb and bf70daa.

📒 Files selected for processing (7)
  • api/static/css/buttons.css (2 hunks)
  • api/static/css/menu.css (3 hunks)
  • api/static/css/responsive.css (2 hunks)
  • api/static/js/app.js (4 hunks)
  • api/static/js/modules/schema.js (1 hunks)
  • api/static/js/modules/ui.js (2 hunks)
  • api/templates/components/left_toolbar.j2 (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • api/static/js/modules/schema.js
  • api/static/css/buttons.css
  • api/templates/components/left_toolbar.j2
  • api/static/css/menu.css
  • api/static/js/app.js
  • api/static/js/modules/ui.js
🧰 Additional context used
📓 Path-based instructions (1)
api/static/**

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store frontend static assets under api/static/

Files:

  • api/static/css/responsive.css
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e-tests
  • GitHub Check: e2e-tests

Comment on lines 20 to 34
/* Menu Responsive */
@media (max-width: 768px) {
#menu-container {
.sidebar-container {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 999;
}
#menu-container.open {

.sidebar-container.open {
width: 80vw;
padding: 15px;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Repo-wide cleanup after selector rename.

To prevent similar regressions, grep for leftover #menu-container, #side-menu-button, and #theme-toggle-btn occurrences across CSS/JS/templates and update/remove them.


🏁 Script executed:

#!/bin/bash
# Find stale selectors/ids after the toolbar/sidebar refactor
set -euo pipefail
echo "Searching for legacy selectors..."
rg -n -S -C2 --glob 'api/**' '#menu-container|#side-menu-button|#theme-toggle-btn'

echo "Check for sidebar open-state rules that may need mobile overrides..."
rg -n -S -C2 --glob 'api/**' '\.sidebar-container\.open'

Length of output: 1955


Cleanup legacy selectors and ensure consistent sidebar styles

The refactor renamed menu and toolbar elements, but the following leftover selectors and duplicate styles remain and should be removed or updated:

  • api/static/js/modules/config.js

    • sideMenuButton: '#side-menu-button'
    • menuContainer: '#menu-container'
  • api/static/css/responsive.css

    • Mobile rule targeting #menu-container.open ~ #chat-container (lines 15–17)
  • api/static/css/buttons.css

    • #theme-toggle-btn selector (lines 139–141)

Additionally, there’s a duplicate .sidebar-container.open rule in api/static/css/menu.css (lines 19–21) with width: 30dvw; padding: 20px; that conflicts with the mobile override in responsive.css. Please remove or consolidate these styles so the sidebar’s open state is governed by a single, consistent media-query rule.

🤖 Prompt for AI Agents
api/static/css/responsive.css (around lines 20–34) plus related files:
api/static/js/modules/config.js (sideMenuButton/menuContainer),
api/static/css/responsive.css (mobile rule targeting #menu-container.open ~
#chat-container at ~lines 15–17), api/static/css/buttons.css (selector
#theme-toggle-btn at ~lines 139–141), and api/static/css/menu.css (duplicate
.sidebar-container.open at lines 19–21): remove the legacy selectors from
config.js (delete or update sideMenuButton and menuContainer entries), remove
the obsolete #menu-container.open ~ #chat-container mobile rule from
responsive.css, delete the stray #theme-toggle-btn selector from buttons.css,
and remove the duplicate .sidebar-container.open rule in menu.css; then
consolidate the sidebar open-state styling into one media-query rule (keep the
responsive.css mobile rule as the single source of truth—e.g., width: 80vw and
padding: 15px for .sidebar-container.open) so there is no conflicting duplicate
across files.

Comment on lines +30 to 34
.sidebar-container.open {
width: 80vw;
padding: 15px;
}
}
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

Explicitly override desktop open-state offset on mobile.

Cross-file context notes .sidebar-container.open has left: 48px in menu.css. Without overriding here, the drawer may be shifted on mobile. Set left: 0 in the mobile open state to ensure a true overlay.

   .sidebar-container.open {
+    left: 0;
     width: 80vw;
     padding: 15px;
   }
📝 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
.sidebar-container.open {
width: 80vw;
padding: 15px;
}
}
.sidebar-container.open {
left: 0;
width: 80vw;
padding: 15px;
}
}
🤖 Prompt for AI Agents
In api/static/css/responsive.css around lines 30-34, the mobile open-state for
.sidebar-container.open currently sets width and padding but doesn't override
the desktop left: 48px from menu.css; update this mobile rule to set left: 0 for
the .sidebar-container.open selector (and if menu.css uses higher specificity or
inline rules, add !important to the left: 0 declaration) so the drawer becomes a
true full-overlay on mobile.

@gkorland gkorland merged commit 2c95b66 into main Aug 21, 2025
11 of 13 checks passed
@gkorland gkorland deleted the toolbar branch August 21, 2025 07:39
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.

Add support for "Show Graph"

2 participants