Skip to content

Feat node graph#93

Open
Its4Nik wants to merge 18 commits into88-refactor-themesidebarcontext-to-separate-ui-state-remove-themeprops-bag-and-avoid-no-op-defaultsfrom
feat-node-graph
Open

Feat node graph#93
Its4Nik wants to merge 18 commits into88-refactor-themesidebarcontext-to-separate-ui-state-remove-themeprops-bag-and-avoid-no-op-defaultsfrom
feat-node-graph

Conversation

@Its4Nik
Copy link
Owner

@Its4Nik Its4Nik commented Mar 17, 2026

Summary by Sourcery

Add an infrastructure graph feature visualising clients, hosts, DockNodes and containers, refactor frontend API/mutation hooks to use shared Eden-based utilities, and extend backend and typings to support graph data, host details, dashboards, and configuration updates.

New Features:

  • Introduce an interactive infrastructure graph page backed by new graph API routes and layout calculation utilities.
  • Add reusable graph node components and styling in the UI library for clients, hosts, DockNodes and containers.
  • Allow configuring a default dashboard and expose dashboard CRUD and config routes and hooks on the frontend and backend.

Bug Fixes:

  • Make worker request handling more robust by returning structured errors on timeouts and crashes instead of throwing.
  • Ensure host listing and metrics correctly include client IDs and network details, and fix various minor logging and request header edge cases.

Enhancements:

  • Refine layout, navbar, sidebar and theme handling including global theme sidebar management, new fonts, and additional graph color tokens.
  • Unify frontend data fetching and mutations around the shared Eden React utilities and new domain-specific mutation hooks for themes, config, plugins, repos, Docker clients/hosts and DockNodes.
  • Improve Docker client monitoring and container mapping by tracking client IDs in container info and propagating them through monitoring managers and mappers.
  • Add new API endpoints and type-safe models for graph data, config additional settings, Docker hosts/clients, and DockNode access, and tighten sqlite wrapper typings.
  • Update dev tooling, dependencies, and scripts including React/Turbo/Bun typings and a Dockerless dev shortcut script.

Its4Nik and others added 15 commits March 11, 2026 14:52
…h DockNodes and containers

This commit introduces a comprehensive update to the infrastructure graph visualization, expanding it to include DockNodes and individual Docker containers. This enhancement provides a more complete overview of the monitored environment by integrating all layers of infrastructure into a single, interactive graph.

**Key changes include:**

*   **API Enhancements (`apps/api`):**
    *   Introduced new `calculateNodeLayout` and `reachableStatus` helpers to dynamically arrange and standardize the status of all node types (clients, hosts, docknodes, containers).
    *   Added robust `GraphModel` schemas for API request/response validation, including definitions for new node types (docknode, container) and their respective data structures.
    *   The `/graph` API endpoint now aggregates data from clients, hosts, docknodes, and containers, processing them into a React Flow-compatible format for the frontend.
    *   Centralized the `DockNodeHandler` instance, improving resource management and consistency.

*   **Frontend UI Overhaul (`apps/dockstat`):**
    *   Integrated `@xyflow/react` for a powerful and interactive graph visualization experience.
    *   Implemented dynamic node types (`client`, `host`, `docknode`, `container`) with distinct visual representations and associated data.
    *   Developed a detailed `NodeDetailsPanel` to display contextual information for selected nodes, including IP, port, status, image, and IDs.
    *   Updated `StatsDisplay` and `Legend` components to accurately reflect the expanded infrastructure components.
    *   Added graph controls like `fitView`, background dots, and snap-to-grid functionality for better usability.

*   **Docker Client Improvements (`packages/docker-client`):**
    *   Modified container mapping to include `clientId`, enabling correct association of containers with their respective client instances in the graph.
    *   Introduced a new `pingHost` method to check the reachability of individual hosts, improving granular status monitoring.
    *   Enhanced worker message handling and error reporting for greater robustness.
This commit introduces a new, interactive infrastructure graph page to visualize the entire Docker client ecosystem.

The backend has been refactored to use the `dagre` library for automated and more robust node layout, replacing the previous manual positioning logic. The `/api/graph` endpoint now provides comprehensive data, including clients, hosts, docknodes, and newly added container information, leveraging enhanced data types.

Key changes include:
- **Backend (`apps/api`):** Migrated graph layout calculation from a custom algorithm to `dagre`. New graph calculator, helper functions, and types were introduced. The `/api/graph` response now includes detailed container information.
- **Frontend (`apps/dockstat`):** Implemented a dedicated `/graph` page using React Flow. New UI components like `GraphFlow`, `NodeDetailsPanel`, `Legend`, and `StatsDisplay` were created to render the graph and its associated data dynamically.
- **`docker-client` package:** Modified container information mapping to include `clientId`, ensuring containers are correctly associated with their respective clients. Host information was also enriched with `host` and `port` details for better graph representation.
- **`typings` package:** Updated `DOCKER.ContainerInfo` to include `clientId` and `DockerClientManagerCore.getAllHosts` return type to include `host` and `port`.
- **`ui` package:** Extended UI components with new styling variables for graph nodes and introduced a `custom` card variant for flexible node rendering.
- **Navigation:** The new graph page is accessible via the sidebar navigation.
- Integrate Geist font family across the application for a modern and consistent typography.
- Redesign graph visualization components (client, host, docknode, container) with an updated aesthetic.
- Implement a comprehensive theming system for graph elements, utilizing new CSS variables for backgrounds, borders, and text colors (e.g., `--color-graph-client-card-bg`).
- Refactor graph node components into dedicated files for better modularity and maintainability.
- Update the graph `Legend` and `StatsDisplay` components to leverage the new theme variables, replacing hardcoded color classes.
- Add "Container" to the graph legend and include a "Containers" count in the stats display.
- Configure Vite's public directory for serving new font assets and update `bun.lock` with new dependencies.
…anagement API and frontend mutations

- **Graph Enhancements:**
  - Introduced a new `dockstat` edge type to display container states (e.g., "exited", "paused") directly on edges connecting hosts to containers.
  - Implemented `DockStatLabelEdge` custom edge component to render these state labels using `Card` elements, providing clear visual cues for container status.
  - Disabled manual edge reconnection (`edgesReconnectable={false}`) and handle connectability (`isConnectable={false}`) in `ReactFlow` to enforce a data-driven graph structure.
  - Updated container edge stroke colors to utilize CSS variables (`--color-success`, `--color-error`) for consistent theming.
  - Removed `snapToGrid` from the graph flow for a smoother interaction experience.

- **API and Frontend Refactoring:**
  - **API Endpoints:** Transformed Docker client and host management routes to align with RESTful conventions.
    - `/client/register`, `/client/update`, `/client/delete` are now consolidated under `POST /client`, `PATCH /client`, `DELETE /client` respectively.
    - `/hosts/add`, `/hosts/update`, `/hosts/delete` are now consolidated under `POST /hosts`, `PATCH /hosts`, `DELETE /hosts` respectively.
  - **Frontend Mutations:** Created dedicated React hooks (`useDockerClientMutations`, `useDockerHostMutations`) to encapsulate API interaction logic, invalidation strategies, and toast notifications for Docker client and host operations.
  - Migrated `AddClient` and `AddHost` components to leverage these new, centralized mutation hooks, improving code organization and reusability.
  - Standardized the usage of `eden.useEdenMutation` for all plugin-related mutations, enhancing developer experience.

- **Minor Improvements:**
  - Ensured consistent semicolon usage across several API files.
  - Adjusted the global font weight for the "Geist" font in `index.css`.
  - Refactored the `Layout` component by moving its definition to `index.tsx` for a cleaner structure.
  - Added `overflow-y-scroll` to the sidebar `Card` to handle content overflow gracefully.
- Introduced dedicated mutation hooks (e.g., `useConfigMutations`, `useDockNodeMutations`, `usePluginMutations`, `useRepoMutations`, `useThemeMutations`) to consolidate data modification logic and improve maintainability.
- Migrated existing components and pages to utilize these new mutation hooks, replacing direct `useEdenMutation` calls.
- Standardized `react-query` data fetching by transitioning to `eden.useEdenQuery` from `@dockstat/utils/react`, leading to the deprecation and removal of the local `useEdenQuery` hook.
- Applied consistent code style by removing semicolons from import and variable declarations across the codebase.
- Enhanced accessibility in the dashboard by converting the `WidgetCard` interactive element from a `div` to a `button`.
- Cleaned up the `/regions` API endpoint by removing an unused `body` parameter.
Introduces the core API infrastructure for managing dashboards.

- The `@dockstat/widget-handler` package now includes server-side components (`src/server`) providing a `DashboardHandler` for database interactions and Elysia routes.
- A new `dashboards` SQLite table is created to persist dashboard configurations, including grid layout, widget instances, and settings.
- The `apps/api` integrates these new dashboard routes, exposing endpoints for CRUD (Create, Read, Update, Delete) operations on dashboards.
- Elysia `t` schemas are defined within `widget-handler/src/server/model.ts` to ensure robust validation of dashboard data payloads for all API interactions.
- Adds necessary dependencies (`@dockstat/logger`, `@dockstat/sqlite-wrapper`, `elysia`) to the `widget-handler` package.
…shboard support [DASH-01]

- Implement backend API endpoint `/config/defaultDashboard` to persist user-selected default dashboards.
- Add new Dashboard management page allowing users to create, rename, delete, and set dashboards as default.
- Update the home index page to dynamically load the configured default dashboard or fall back to a placeholder.
- Refactor the WidgetDrawer in the widget-handler package from a custom slide-out to a standard Modal-based palette for better UX.
- Extend the database schema and TypeBox models to include default dashboard persistence in additionalSettings.
- Update the sidebar navigation to include a direct link to the new Dashboards management view.
- Upgrade @dockstat/sqlite-wrapper to v1.3.13 and apply project-wide formatting updates (semicolon removal).
This commit removes the entire dashboard infrastructure and the `@dockstat/widget-handler` package from the workspace. It also includes minor UX improvements for the host configuration interface.

- Deleted the `@dockstat/widget-handler` package, including its server-side handlers and frontend components.
- Removed dashboard-related routes and logic from the API and the main frontend application.
- Replaced the dashboard rendering on the home page with a temporary placeholder.
- Updated the `AddHost` component to display client names alongside IDs in the selection dropdown for better usability.
- Cleaned up workspace configuration files, lockfiles, and TypeScript references to reflect the package removal.
… [N/A]

Apply consistent code formatting across the monorepo. This change addresses trailing commas in TypeScript configuration files and improves the readability of React components in the dockstat application by adjusting line breaks and indentation.

- Cleaned up trailing commas in apps/api/tsconfig.json and tsconfig.base.json.
- Reformatted props and JSX structure in AddHost.tsx and configure.tsx for better readability.
- Removed redundant whitespace and parentheses in the dockstat IndexPage.
Copilot AI review requested due to automatic review settings March 17, 2026 21:03
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 17, 2026

Reviewer's Guide

Implements a new infrastructure graph feature (backend graph API and layout calculation plus ReactFlow-based UI with custom node/edge components), centralizes Eden-based data mutations and queries into shared hooks, enriches Docker client/host/container types and monitoring with client IDs and per-host ping, extends configuration and database schemas (including default dashboard), and makes various UI, routing, and API refinements to support the new graph and cleaner REST conventions.

ER diagram for updated configuration and host/container data

erDiagram
  DockStatConfigTable ||--|{ AdditionalSettings : has
  DockerHost ||--|{ DockerContainer : hosts

  DockStatConfigTable {
    int id
    string name
    json keys
    json additionalSettings
    json hotkeys
  }

  AdditionalSettings {
    boolean showBackendRamUsageInNavbar
    string defaultDashboard
  }

  DockerHost {
    int id
    int clientId
    string name
    string host
    int port
    boolean reachable
  }

  DockerContainer {
    string id
    int hostId
    int clientId
    string name
    string image
    string status
    string state
  }
Loading

Class diagram for graph types and UI node components

classDiagram
  class GraphInput {
    +clients: GraphClient[]
    +hosts: GraphHost[]
    +dockNodes: DockNodeArray
    +containers: DOCKER_ContainerInfo[]
  }

  class GraphClient {
    +id: number
    +name: string
    +initialized: boolean
  }

  class GraphHost {
    +id: number
    +name: string
    +clientId: number
    +reachable: boolean
    +host: string
    +port: number
  }

  class DockNodeItem {
    +id: number
    +name: string
    +hostname: string
    +port: number
    +reachable: ReachableStatus
  }

  class ReachableStatus {
    <<enumeration>>
    OK
    NO
    DockNode_not_initialised
  }

  class DOCKER_ContainerInfo {
    +id: string
    +hostId: number
    +clientId: number
    +name: string
    +image: string
    +status: string
    +state: string
  }

  class FlowNode {
    +id: string
    +type: GraphNodeType
    +positionX: number
    +positionY: number
    +data: NodeBaseData
  }

  class NodeBaseData {
    +label: string
    +status: string
    +ipAddress: string
    +port: number
  }

  class ClientNodeData {
    +label: string
    +status: string
    +ipAddress: string
    +port: number
    +clientId: number
  }

  class HostNodeData {
    +label: string
    +status: string
    +ipAddress: string
    +port: number
    +clientId: number
    +hostId: number
  }

  class DockNodeData {
    +label: string
    +status: string
    +ipAddress: string
    +port: number
    +dockNodeId: number
  }

  class ContainerNodeData {
    +label: string
    +status: string
    +image: string
    +state: string
    +hostId: number
    +containerId: string
  }

  class FlowEdge {
    +id: string
    +edgesReconnectable: boolean
    +source: string
    +target: string
    +animated: boolean
    +styleStroke: string
    +label: string
    +type: string
  }

  class GraphNodeType {
    <<enumeration>>
    client
    host
    docknode
    container
  }

  class GraphModel_GraphNodeSchema {
    +id: string
    +type: GraphNodeType
    +positionX: number
    +positionY: number
    +data_label: string
    +data_status: string
    +data_ipAddress: string
    +data_port: number
    +data_image: string
    +data_clientId: number
    +data_hostId: number
  }

  class GraphModel_GraphEdgeSchema {
    +id: string
    +source: string
    +target: string
    +animated: boolean
    +style_stroke: string
  }

  class ClientNodeComponent {
    +render(props: Node_ClientNodeData): JSXElement
  }

  class HostNodeComponent {
    +render(props: Node_HostNodeData): JSXElement
  }

  class DockNodeComponent {
    +render(props: Node_DockNodeData): JSXElement
  }

  class ContainerNodeComponent {
    +render(props: Node_ContainerNodeData): JSXElement
  }

  class nodeTypes_map {
    +client: ClientNodeComponent
    +host: HostNodeComponent
    +docknode: DockNodeComponent
    +container: ContainerNodeComponent
  }

  class calculateNodeLayout {
    +calculateNodeLayout(input: GraphInput): GraphLayoutResult
  }

  class GraphLayoutResult {
    +nodes: FlowNode[]
    +edges: FlowEdge[]
  }

  GraphInput --> GraphClient
  GraphInput --> GraphHost
  GraphInput --> DockNodeItem
  GraphInput --> DOCKER_ContainerInfo

  DockNodeItem --> ReachableStatus

  FlowNode --> NodeBaseData
  ClientNodeData --|> NodeBaseData
  HostNodeData --|> NodeBaseData
  DockNodeData --|> NodeBaseData

  FlowNode --> GraphNodeType

  GraphModel_GraphNodeSchema --> GraphNodeType
  GraphModel_GraphNodeSchema --> NodeBaseData

  GraphModel_GraphEdgeSchema --> FlowEdge

  calculateNodeLayout --> GraphLayoutResult
  calculateNodeLayout --> FlowNode
  calculateNodeLayout --> FlowEdge

  ClientNodeComponent --> ClientNodeData
  HostNodeComponent --> HostNodeData
  DockNodeComponent --> DockNodeData
  ContainerNodeComponent --> ContainerNodeData

  nodeTypes_map --> ClientNodeComponent
  nodeTypes_map --> HostNodeComponent
  nodeTypes_map --> DockNodeComponent
  nodeTypes_map --> ContainerNodeComponent
Loading

File-Level Changes

Change Details Files
Add infrastructure graph backend API and layout calculation using dagre, with typed graph models.
  • Introduce graph calculation module that builds a dagre graph from clients, hosts, docknodes, and containers and returns positioned nodes/edges for ReactFlow.
  • Add GraphElysia routes under /graph that aggregate data from DockerClientManager and DockNode handler, map reachability status, and return typed graph data.
  • Define strongly-typed graph models and helper utilities for node IDs, edge creation, positioning, and reachable status mapping, plus a shared DockNode handler instance.
apps/api/src/routes/graph/index.ts
apps/api/src/graph/graphCalculator.ts
apps/api/src/graph/helper.ts
apps/api/src/graph/types.ts
apps/api/src/graph/reachableStatus.ts
apps/api/src/graph/index.ts
apps/api/src/docker/docknode.ts
apps/api/src/models/graph.ts
apps/api/src/index.ts
Implement ReactFlow-based infrastructure graph UI with custom node/edge components and statistics panel, and wire it into the frontend router and navbar.
  • Create GraphFlow component that fetches graph data via Eden, manages ReactFlow nodes/edges, handles node selection, and renders background, edge types, and stats panel.
  • Add custom ReactFlow node components for clients, hosts, docknodes, and containers with graph-specific theming, plus a labeled custom edge type and legend/details components.
  • Expose graph nodeTypes from the UI package and register them in GraphFlow, add a /graph page wrapped in ReactFlowProvider, and link it from navbar sidebar paths and router.
  • Adjust global styles and theme variables for graph colors, ReactFlow attribution styling, and font setup (Geist families).
apps/dockstat/src/components/graph/flow.tsx
apps/dockstat/src/components/graph/customEdge.tsx
apps/dockstat/src/components/graph/legend.tsx
apps/dockstat/src/components/graph/nodeDetails.tsx
apps/dockstat/src/components/graph/statsDisplay.tsx
apps/dockstat/src/pages/graph/index.tsx
packages/ui/src/components/Graph/index.tsx
packages/ui/src/components/Graph/nodes/client.tsx
packages/ui/src/components/Graph/nodes/container.tsx
packages/ui/src/components/Graph/nodes/docknode.tsx
packages/ui/src/components/Graph/nodes/host.tsx
packages/ui/src/components/Navbar/consts.ts
apps/dockstat/src/router.tsx
apps/dockstat/src/index.css
packages/ui/src/App.css
apps/dockstat/vite.config.ts
apps/dockstat/package.json
Enrich Docker client, host, and container types and monitoring with client IDs, host connectivity details, and more resilient worker communication.
  • Extend ContainerInfo with clientId and propagate this through mapping helpers, container mixins, and monitoring managers (container events, metrics, and event streams).
  • Add per-host pingHost method on client Hosts mixin, add worker request type pingHost, and adjust worker message handler to support it.
  • Enhance DockerClientManagerCore getAllHosts to include host and port, update manager Hosts mixin to pass these fields through, and adjust HostHandler.getHosts to use a select to retrieve all columns.
  • Change sendWorkerMessage to be non-generic, resolve with error responses instead of throwing on timeout/worker error, and update manager sendRequest usage accordingly.
  • Wire clientId into MonitoringManager construction from DockerClientBase and Monitoring mixin so monitors can attribute data to the correct client.
packages/typings/src/docker-client.ts
packages/docker-client/src/client/utils/mapContainerInfo.ts
packages/docker-client/src/client/mixins/containers/index.ts
packages/docker-client/src/client/managers/monitoring/index.ts
packages/docker-client/src/client/managers/monitoring/eventStreamMonitor.ts
packages/docker-client/src/client/managers/monitoring/monitors/ContainerMetrics.ts
packages/docker-client/src/client/managers/monitoring/monitors/ContainerEvents.ts
packages/docker-client/src/client/mixins/core/base.ts
packages/docker-client/src/client/mixins/monitoring/index.ts
packages/docker-client/src/client/mixins/hosts/index.ts
packages/docker-client/src/client/managers/host-handler/index.ts
packages/docker-client/src/manager/core.ts
packages/docker-client/src/manager/hosts.ts
packages/docker-client/src/manager/utils/sendWorkerMessage.ts
packages/docker-client/src/shared/types.ts
packages/docker-client/src/_worker.index.ts
Introduce centralized Eden-based mutation/query hooks and migrate existing feature areas (themes, config, plugins, repos, Docker clients/hosts, DockNodes, dashboards) to use them.
  • Add hooks under hooks/mutations for config, dashboards, Docker clients, Docker hosts, DockNodes, plugins, repos, and themes, encapsulating Eden mutation setup including toasts and query invalidation.
  • Refactor multiple components and hooks (theme provider, theme sidebar, settings general/hotkeys, plugin browser, plugin template/actions/loaders, repo card, node page, clients configure/index, AddClient, AddHost, additional settings provider, pin mutations, extensions index) to use these new mutation/query helpers and eden.useEdenQuery/useEdenRouteMutation from @dockstat/utils/react.
  • Export a barrel file hooks/mutations/index.ts and re-export the legacy useEdenMutation as edenMutation for compatibility.
apps/dockstat/src/hooks/mutations/config.ts
apps/dockstat/src/hooks/mutations/dashboard.ts
apps/dockstat/src/hooks/mutations/docker-client.ts
apps/dockstat/src/hooks/mutations/docker-host.ts
apps/dockstat/src/hooks/mutations/node.ts
apps/dockstat/src/hooks/mutations/plugin.ts
apps/dockstat/src/hooks/mutations/repo.ts
apps/dockstat/src/hooks/mutations/theme.ts
apps/dockstat/src/hooks/mutations/index.ts
apps/dockstat/src/providers/theme.tsx
apps/dockstat/src/providers/themeSidebar.tsx
apps/dockstat/src/providers/additionalSettings.tsx
apps/dockstat/src/components/settings/general/sections/useGeneralSettings.tsx
apps/dockstat/src/components/settings/hotkeys.tsx
apps/dockstat/src/components/extensions/RepoCard.tsx
apps/dockstat/src/pages/extensions/index.tsx
apps/dockstat/src/pages/extensions/plugins.tsx
apps/dockstat/src/pages/node/index.tsx
apps/dockstat/src/pages/clients/index.tsx
apps/dockstat/src/pages/clients/configure.tsx
apps/dockstat/src/components/clients/configure/AddClient.tsx
apps/dockstat/src/components/clients/configure/AddHost.tsx
apps/dockstat/src/layout/hooks/usePinMutations.ts
apps/dockstat/src/hooks/plugins/usePluginTemplate.ts
apps/dockstat/src/hooks/plugins/usePluginActions.ts
apps/dockstat/src/hooks/plugins/usePluginLoaders.ts
apps/dockstat/src/hooks/useEdenQuery.ts
apps/dockstat/src/hooks/eden/useEdenMutation.ts
apps/dockstat/src/contexts/theme.ts
Extend configuration/database capabilities with default dashboard support and tighten type schemas.
  • Add /db/config/defaultDashboard route that updates additionalSettings.defaultDashboard while preserving existing settings and returns a typed response.
  • Extend DatabaseModel.additionalSettingsRes and DockStatConfigTable additionalSettings schema to include optional defaultDashboard, and set defaultConfig.additionalSettings.defaultDashboard to undefined.
  • Add dashboard mutation hook with create/update/delete and setDefaultDashboard operations wired to the new route and dashboards API.
  • Tighten sqlite-wrapper createTable typing and listBackups shape, and bump its package version.
apps/api/src/routes/db.ts
apps/api/src/models/database.ts
packages/typings/src/typebox/db.ts
packages/db/defaults.ts
apps/dockstat/src/hooks/mutations/dashboard.ts
packages/sqlite-wrapper/src/index.ts
packages/sqlite-wrapper/package.json
Align Docker client and host REST APIs with more conventional HTTP verbs and adjust frontend callers accordingly.
  • Change Docker client routes from /register,/update,/delete POSTs to POST /, PATCH /, and DELETE /, and Docker host routes from /add,/update,/delete POSTs to POST /, PATCH /, and DELETE /.
  • Update frontend Docker host/client mutation hooks and AddHost/AddClient usage to target the new routes.
  • Ensure host CRUD hooks invalidate the hosts list and provide descriptive toasts for add/update/delete.
apps/api/src/routes/docker/client.ts
apps/api/src/routes/docker/hosts.ts
apps/dockstat/src/hooks/mutations/docker-client.ts
apps/dockstat/src/hooks/mutations/docker-host.ts
apps/dockstat/src/components/clients/configure/AddClient.tsx
apps/dockstat/src/components/clients/configure/AddHost.tsx
Tighten and polish UI components and layout behavior for Navbar, Sidebar, forms, cards, modals, links, and logs.
  • Adjust Navbar card width and NavLink usage, enable vertical scrolling in Sidebar card, and expose new graph/dashboards paths; improve ReactFlow attribution styling and card layout for graph usage.
  • Extend Card with a custom variant, allow Modal title to be ReactNode, add Select.rawOnChange for raw event handling, convert Input to a plain function component, and ensure NavLink-based LinkWithIcon uses exact matching.
  • Tweak log error toast formatting and console.error logging, add rawOnChange wiring in Select, and minor theme manager effect dependency fixes.
packages/ui/src/components/Navbar/Navbar.tsx
packages/ui/src/components/Sidebar/Sidebar.tsx
apps/dockstat/src/layout/hooks/useLogs.tsx
packages/ui/src/components/Card/Card.tsx
packages/ui/src/components/Modal/Modal.tsx
packages/ui/src/components/Forms/Input.tsx
packages/ui/src/components/Forms/Selection.tsx
packages/ui/src/components/Link/Link.tsx
apps/dockstat/src/layout/hooks/useThemeManager.ts
apps/dockstat/src/index.css
Miscellaneous project-wide cleanups, dependency updates, and placeholder changes.
  • Replace the main IndexPage content with a temporary placeholder and remove direct backend status fetching.
  • Update API and root package.json dependencies (add dagre, @types/dagre, ReactFlow/Geist in frontend and UI, bump turbo, @types/bun, @types/react, adjust catalog react-dom), and add a dev:dockstat:no-docker script.
  • Improve request/metrics logging to handle optional headers safely, adjust default table parsing in HostHandler.getHosts, and tweak Docker worker ok response formatting.
apps/dockstat/src/pages/index.tsx
apps/api/package.json
package.json
apps/api/src/middleware/metrics/helper.ts
apps/api/src/handlers/requestLogger.ts
packages/docker-client/src/client/managers/host-handler/index.ts
packages/docker-client/src/_worker.index.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 9 issues, and left some high level feedback:

  • The new graph API response (GraphModel.GraphDataSchema) doesn’t fully match what calculateNodeLayout actually returns (e.g. node data can contain dockNodeId, containerId, state, etc., and edges can have type: 'dockstat', label, and edgesReconnectable), which may cause runtime validation issues—consider aligning the schema with the concrete output or trimming extra fields before returning.
  • The updated / frontend route now renders only <div>TEMP</div> instead of the previous backend status/health summary; if this is not intentional, consider restoring at least a minimal status view or gating the temporary placeholder behind a feature flag.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new graph API response (`GraphModel.GraphDataSchema`) doesn’t fully match what `calculateNodeLayout` actually returns (e.g. node `data` can contain `dockNodeId`, `containerId`, `state`, etc., and edges can have `type: 'dockstat'`, `label`, and `edgesReconnectable`), which may cause runtime validation issues—consider aligning the schema with the concrete output or trimming extra fields before returning.
- The updated `/` frontend route now renders only `<div>TEMP</div>` instead of the previous backend status/health summary; if this is not intentional, consider restoring at least a minimal status view or gating the temporary placeholder behind a feature flag.

## Individual Comments

### Comment 1
<location path="apps/dockstat/src/layout/hooks/useLogs.tsx" line_range="28" />
<code_context>
-          </p>
+          <div>
+            <p>A server error occurred!</p>
+            <span className="text-accent">{logMessage.name}</span>]
+          </div>
         ),
</code_context>
<issue_to_address>
**issue (bug_risk):** There is an extra closing bracket in the error toast title.

The JSX currently ends with `</span>]`, which will render an unintended `]` in the UI. Please remove the trailing bracket so only the span content is shown.
</issue_to_address>

### Comment 2
<location path="apps/dockstat/src/components/clients/configure/AddHost.tsx" line_range="23" />
<code_context>
     name: "",
     hostname: "",
-    clientId: registeredClients[0] || 0,
+    clientId: registeredClients[0].clientId || 0,
     port: 2375,
   })
</code_context>
<issue_to_address>
**issue (bug_risk):** Accessing `registeredClients[0].clientId` can throw when the list is empty.

If this component mounts with an empty `registeredClients` array, `registeredClients[0]` will be `undefined` and accessing `.clientId` will throw. Consider guarding it, e.g. `registeredClients[0]?.clientId ?? 0`, or using a safer default value.
</issue_to_address>

### Comment 3
<location path="apps/api/src/models/graph.ts" line_range="18-25" />
<code_context>
+      x: t.Number(),
+      y: t.Number(),
+    }),
+    data: t.Object({
+      label: t.String(),
+      status: t.String(),
+      ipAddress: t.Optional(t.String()),
+      port: t.Optional(t.Number()),
+      image: t.Optional(t.String()),
+      clientId: t.Optional(t.Number()),
+      hostId: t.Optional(t.Number()),
+    }),
+  })
</code_context>
<issue_to_address>
**issue (bug_risk):** Graph node schema is missing fields that `calculateNodeLayout` includes in `data`.

`GraphNodeSchema.data` only permits `label`, `status`, `ipAddress`, `port`, `image`, `clientId`, and `hostId`, but `calculateNodeLayout` also writes `dockNodeId`, `containerId`, `state`, etc. Since `t.Object` disallows additional properties by default, the handler response may fail validation or need unsafe casting. Please either update `GraphNodeSchema.data` to cover these fields or adjust `calculateNodeLayout` to emit only schema-defined properties.
</issue_to_address>

### Comment 4
<location path="apps/api/src/models/graph.ts" line_range="29-34" />
<code_context>
+    }),
+  })
+
+  export const GraphEdgeSchema = t.Object({
+    id: t.String(),
+    source: t.String(),
+    target: t.String(),
+    animated: t.Optional(t.Boolean()),
+    style: t.Optional(
+      t.Object({
+        stroke: t.Optional(t.String()),
</code_context>
<issue_to_address>
**issue (bug_risk):** Graph edge schema does not match the edge shape produced by `calculateNodeLayout`.

`FlowEdge` and `calculateNodeLayout` include fields like `edgesReconnectable`, `label`, and container `type: "dockstat"`, but `GraphEdgeSchema` only validates `id`, `source`, `target`, `animated`, and `style.stroke`. This discrepancy can cause validation failures at runtime. Either extend `GraphEdgeSchema` to cover these fields or ensure `calculateNodeLayout` removes them before returning the edges.
</issue_to_address>

### Comment 5
<location path="apps/dockstat/src/components/graph/flow.tsx" line_range="66" />
<code_context>
+    )
+  }
+
+  const hasNoData = !data || (data && "nodes" in data && data.nodes?.length === 0)
+
+  return (
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `hasNoData` check will treat error responses without `nodes` as "no data" instead of an error.

When the backend returns the 400 shape `{ success: false, error }`, the eden wrapper may still expose that as `data`. In that case, `"nodes" in data` is false, so `hasNoData` is false and neither the error nor empty state renders. Consider explicitly handling the success/error shape when computing `hasNoData` (e.g. `if ('success' in data && data.success === false) ...`), consistent with how you decide whether to call `setNodes`/`setEdges`.

```suggestion
  // Treat explicit backend errors (`{ success: false, error }`) as errors, not "no data".
  // Only consider it "no data" when:
  // - there is no `data`, or
  // - the request was successful (`success` absent or true) and there are zero nodes.
  const hasNoData =
    !data ||
    (("success" in data ? data.success : true) &&
      "nodes" in data &&
      Array.isArray((data as any).nodes) &&
      data.nodes.length === 0)
```
</issue_to_address>

### Comment 6
<location path="packages/ui/src/components/Forms/Selection.tsx" line_range="18" />
<code_context>
   disabled?: boolean
   placeholder?: string
   value?: string
+  rawOnChange?: (e: React.ChangeEvent<HTMLSelectElement, HTMLSelectElement>) => void
   onChange?: (value: string) => void
   className?: string
</code_context>
<issue_to_address>
**issue (bug_risk):** The `rawOnChange` type uses an incorrect `React.ChangeEvent` generic signature.

`React.ChangeEvent` only takes a single generic for the target element, so this should be `React.ChangeEvent<HTMLSelectElement>`. The two-parameter form is invalid and may either produce a type error or be ignored by TypeScript.
</issue_to_address>

### Comment 7
<location path="apps/dockstat/src/layout/index.tsx" line_range="71-63" />
<code_context>
-
-            return pinMutation.mutateAsync(input)
-          },
-          unpin: (input: { path: string; slug: string }) => {
-            toast({
-              title: `Unpinned "${input.slug}"!`,
-              description: (
-                <span>
-                  Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
-                </span>
-              ),
</code_context>
<issue_to_address>
**nitpick (bug_risk):** Unpin toast message still says "Added a new pinned link".

The `unpin` toast currently reuses the `pin` description (`Added a new pinned link`). Please update the description to clearly state that a link was unpinned/removed instead.
</issue_to_address>

### Comment 8
<location path="apps/dockstat/src/layout/index.tsx" line_range="10" />
<code_context>
-import { toast } from "@/lib/toast"
-import { useLayout } from "../hooks/useLayout"
-
-export function Layout({ children }: { children: React.ReactNode }) {
-  const {
-    ramUsage,
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the Layout module by extracting helper functions for pin/unpin handlers and precomputed props, and by aliasing the useLayout hook to avoid duplicate import/export in the same file.

You can trim the complexity of `Layout` without changing behavior by pulling the small bits of logic out of the JSX and avoiding the duplicate `useLayout` import/export.

### 1. Extract pin/unpin logic out of JSX

Right now, the `mutationFn` object embeds toast + mutation logic inline. Moving it into a helper keeps the JSX declarative and makes behavior easier to test.

```ts
type PinInput = { path: string; slug: string }

function createPinMutationHandlers({
  pinMutation,
  unPinMutation,
  isBusy,
}: {
  pinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
  unPinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
  isBusy: boolean
}) {
  const notify = (action: "Pinned" | "Unpinned", input: PinInput) =>
    toast({
      title: `${action} "${input.slug}"!`,
      description: (
        <span>
          Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
        </span>
      ),
      variant: "success",
    })

  return {
    pin: (input: PinInput) => {
      notify("Pinned", input)
      return pinMutation.mutateAsync(input)
    },
    unpin: (input: PinInput) => {
      notify("Unpinned", input)
      return unPinMutation.mutateAsync(input)
    },
    isBusy,
  }
}
```

Then in `Layout`:

```tsx
const mutationFn = createPinMutationHandlers({ pinMutation, unPinMutation, isBusy })

return (
  <div className="bg-main-bg min-h-screen w-screen p-4">
    <Toaster expand position="bottom-right" />
    <Navbar
      // ...
      mutationFn={mutationFn}
      // ...
    />
    {/* ... */}
  </div>
)
```

This keeps the JSX mostly “data-shaped” props instead of embedded behavior.

### 2. Precompute shaped props above JSX

Similarly, move the small mapping/selection logic into local constants right before the `return`:

```tsx
const sidebarHotkeys = {
  close: config.hotkeys?.["close:sidebar"],
  open: config.hotkeys?.["open:sidebar"],
  toggle: config.hotkeys?.["toggle:sidebar"],
}

const ramUsageForNavbar = config.additionalSettings?.showBackendRamUsageInNavbar
  ? ramUsage
  : undefined

const pluginLinks = frontendPluginRoutes || []
const navLinks = config?.navLinks || []
```

and use them in JSX:

```tsx
<Navbar
  sidebarHotkeys={sidebarHotkeys}
  ramUsage={ramUsageForNavbar}
  pluginLinks={pluginLinks}
  navLinks={navLinks}
  // ...
/>
```

This shortens the JSX, making it easier to scan.

### 3. Avoid duplicate `useLayout` import/re-export in the same file

You can keep the re-export while using a single import locally by aliasing:

```ts
import { useLayout as useLayoutHook } from "./hooks/useLayout"
export { useLayoutHook as useLayout }

export function Layout({ children }: { children: React.ReactNode }) {
  const {
    ramUsage,
    // ...
  } = useLayoutHook()
  // ...
}
```

This removes the double import/re-export smell and clarifies that this file is both a container and a re-export, without changing behavior.
</issue_to_address>

### Comment 9
<location path="apps/api/src/graph/graphCalculator.ts" line_range="17" />
<code_context>
+
+const logger = BaseLogger.spawn("GraphCalc")
+
+export function calculateNodeLayout(input: GraphInput): {
+  nodes: FlowNode[]
+  edges: FlowEdge[]
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring `calculateNodeLayout` into smaller helpers and shared utilities to separate orchestration from per-entity details and logging noise.

You can keep the behavior intact while reducing complexity by extracting a few focused helpers and centralizing the repetitive patterns.

### 1. Split `calculateNodeLayout` into orchestration + helpers

Move node/edge registration and flow graph construction into separate pure helpers. This keeps `calculateNodeLayout` readable at a glance:

```ts
export function calculateNodeLayout(input: GraphInput): { nodes: FlowNode[]; edges: FlowEdge[] } {
  const { clients, hosts, dockNodes, containers } = input;

  logger.info("Starting graph layout calculation...");
  logger.debug(
    `Input counts: Clients=${clients.length}, Hosts=${hosts.length}, DockNodes=${dockNodes.length}, Containers=${containers.length}`
  );

  const g = createDagreGraph();
  registerNodes(g, input);
  registerEdgesForLayout(g, input);

  logger.info("Running Dagre layout algorithm...");
  dagre.layout(g);
  logger.info("Dagre layout complete.");

  const { nodes, edges } = buildFlowGraphFromDagre(g, input);

  logger.info(`Graph layout finished. Generated ${nodes.length} nodes and ${edges.length} edges.`);
  return { nodes, edges };
}

function createDagreGraph(): dagre.graphlib.Graph {
  const g = new dagre.graphlib.Graph();
  g.setGraph({ rankdir: "LR", nodesep: 20, ranksep: 50, marginx: 50, marginy: 50 });
  g.setDefaultEdgeLabel(() => ({}));
  return g;
}

function registerNodes(g: dagre.graphlib.Graph, input: GraphInput): void {
  const { clients, hosts, dockNodes, containers } = input;
  logger.info("Registering nodes...");

  const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
    logger.debug(`Registering node in Dagre: [${type}] ${id}`);
    g.setNode(id, { width: 250, height: 80 });
  };

  for (const c of clients) addNodeToGraph(getClientId(c.id), "Client");
  for (const h of hosts) addNodeToGraph(getHostId(h.clientId, h.id), "Host");
  for (const co of containers) addNodeToGraph(getContainerId(co.id), "Container");
  for (const d of dockNodes) addNodeToGraph(getDockNodeId(d.id), "DockNode");
}

function registerEdgesForLayout(g: dagre.graphlib.Graph, input: GraphInput): void {
  const { hosts, containers } = input;
  logger.info("Registering edges for layout calculation...");

  hosts.forEach((host) => {
    const source = getClientId(host.clientId);
    const target = getHostId(host.clientId, host.id);
    g.setEdge(source, target);
  });

  containers.forEach((container) => {
    const source = getHostId(container.clientId, container.hostId);
    const target = getContainerId(container.id);

    if (g.hasNode(source)) {
      g.setEdge(source, target);
    } else {
      logger.warn(
        `Container ${container.id} references non-existent Host ${source}. Edge skipped.`
      );
    }
  });
}
```

### 2. Factor out repeated Dagre-node → FlowNode mapping

The logic for clients/hosts/containers/dockNodes shares a pattern: resolve Dagre node, guard, build `FlowNode`. You can use a small helper that centralizes the guard and position calculation:

```ts
function buildFlowNode(
  g: dagre.graphlib.Graph,
  nodeId: string,
  type: FlowNode["type"],
  data: FlowNode["data"]
): FlowNode | null {
  const dagreNode = g.node(nodeId);
  if (!dagreNode) {
    logger.error(`Missing node in Dagre output: ${nodeId}`);
    return null;
  }
  return {
    id: nodeId,
    type,
    position: calculatePosition(dagreNode),
    data,
  };
}
```

Then `buildFlowGraphFromDagre` can stay concise:

```ts
function buildFlowGraphFromDagre(
  g: dagre.graphlib.Graph,
  input: GraphInput
): { nodes: FlowNode[]; edges: FlowEdge[] } {
  const { clients, hosts, containers, dockNodes } = input;
  const nodes: FlowNode[] = [];
  const edges: FlowEdge[] = [];

  clients.forEach((client) => {
    const flowNode = buildFlowNode(g, getClientId(client.id), "client", {
      label: client.name || `Client ${client.id}`,
      status: client.initialized ? "online" : "offline",
      clientId: client.id,
    });
    if (flowNode) nodes.push(flowNode);
  });

  hosts.forEach((host) => {
    const hostId = getHostId(host.clientId, host.id);
    const flowNode = buildFlowNode(g, hostId, "host", {
      label: host.name || `Host ${host.id}`,
      status: host.reachable ? "online" : "offline",
      ipAddress: host.host,
      port: host.port,
      clientId: host.clientId,
      hostId: host.id,
    });
    if (flowNode) nodes.push(flowNode);

    edges.push(
      createEdge(
        getClientId(host.clientId),
        hostId,
        host.reachable,
        getEdgeStrokeColor(host.reachable)
      )
    );
  });

  // containers & dockNodes follow same pattern, using buildFlowNode
  // ...
  return { nodes, edges };
}
```

This removes the repeated `g.node(...)` / null-guard / `calculatePosition(...)` boilerplate.

### 3. Gate verbose logging behind a flag

The per-node debug logs are useful but noisy. If `BaseLogger` doesn’t already support it, a simple flag can keep the main algorithm readable while preserving logs:

```ts
const logger = BaseLogger.spawn("GraphCalc");
const DEBUG_GRAPH_LAYOUT = process.env.DEBUG_GRAPH_LAYOUT === "true";

function debugLog(message: string) {
  if (DEBUG_GRAPH_LAYOUT) {
    logger.debug(message);
  }
}

// usage in registerNodes:
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
  debugLog(`Registering node in Dagre: [${type}] ${id}`);
  g.setNode(id, { width: 250, height: 80 });
};
```

This keeps the key `info`/`warn`/`error` logs but hides the verbose details unless explicitly enabled.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

import { toast } from "@/lib/toast"
import { useLayout } from "./hooks/useLayout"

export function Layout({ children }: { children: React.ReactNode }) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider simplifying the Layout module by extracting helper functions for pin/unpin handlers and precomputed props, and by aliasing the useLayout hook to avoid duplicate import/export in the same file.

You can trim the complexity of Layout without changing behavior by pulling the small bits of logic out of the JSX and avoiding the duplicate useLayout import/export.

1. Extract pin/unpin logic out of JSX

Right now, the mutationFn object embeds toast + mutation logic inline. Moving it into a helper keeps the JSX declarative and makes behavior easier to test.

type PinInput = { path: string; slug: string }

function createPinMutationHandlers({
  pinMutation,
  unPinMutation,
  isBusy,
}: {
  pinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
  unPinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
  isBusy: boolean
}) {
  const notify = (action: "Pinned" | "Unpinned", input: PinInput) =>
    toast({
      title: `${action} "${input.slug}"!`,
      description: (
        <span>
          Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
        </span>
      ),
      variant: "success",
    })

  return {
    pin: (input: PinInput) => {
      notify("Pinned", input)
      return pinMutation.mutateAsync(input)
    },
    unpin: (input: PinInput) => {
      notify("Unpinned", input)
      return unPinMutation.mutateAsync(input)
    },
    isBusy,
  }
}

Then in Layout:

const mutationFn = createPinMutationHandlers({ pinMutation, unPinMutation, isBusy })

return (
  <div className="bg-main-bg min-h-screen w-screen p-4">
    <Toaster expand position="bottom-right" />
    <Navbar
      // ...
      mutationFn={mutationFn}
      // ...
    />
    {/* ... */}
  </div>
)

This keeps the JSX mostly “data-shaped” props instead of embedded behavior.

2. Precompute shaped props above JSX

Similarly, move the small mapping/selection logic into local constants right before the return:

const sidebarHotkeys = {
  close: config.hotkeys?.["close:sidebar"],
  open: config.hotkeys?.["open:sidebar"],
  toggle: config.hotkeys?.["toggle:sidebar"],
}

const ramUsageForNavbar = config.additionalSettings?.showBackendRamUsageInNavbar
  ? ramUsage
  : undefined

const pluginLinks = frontendPluginRoutes || []
const navLinks = config?.navLinks || []

and use them in JSX:

<Navbar
  sidebarHotkeys={sidebarHotkeys}
  ramUsage={ramUsageForNavbar}
  pluginLinks={pluginLinks}
  navLinks={navLinks}
  // ...
/>

This shortens the JSX, making it easier to scan.

3. Avoid duplicate useLayout import/re-export in the same file

You can keep the re-export while using a single import locally by aliasing:

import { useLayout as useLayoutHook } from "./hooks/useLayout"
export { useLayoutHook as useLayout }

export function Layout({ children }: { children: React.ReactNode }) {
  const {
    ramUsage,
    // ...
  } = useLayoutHook()
  // ...
}

This removes the double import/re-export smell and clarifies that this file is both a container and a re-export, without changing behavior.


const logger = BaseLogger.spawn("GraphCalc")

export function calculateNodeLayout(input: GraphInput): {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring calculateNodeLayout into smaller helpers and shared utilities to separate orchestration from per-entity details and logging noise.

You can keep the behavior intact while reducing complexity by extracting a few focused helpers and centralizing the repetitive patterns.

1. Split calculateNodeLayout into orchestration + helpers

Move node/edge registration and flow graph construction into separate pure helpers. This keeps calculateNodeLayout readable at a glance:

export function calculateNodeLayout(input: GraphInput): { nodes: FlowNode[]; edges: FlowEdge[] } {
  const { clients, hosts, dockNodes, containers } = input;

  logger.info("Starting graph layout calculation...");
  logger.debug(
    `Input counts: Clients=${clients.length}, Hosts=${hosts.length}, DockNodes=${dockNodes.length}, Containers=${containers.length}`
  );

  const g = createDagreGraph();
  registerNodes(g, input);
  registerEdgesForLayout(g, input);

  logger.info("Running Dagre layout algorithm...");
  dagre.layout(g);
  logger.info("Dagre layout complete.");

  const { nodes, edges } = buildFlowGraphFromDagre(g, input);

  logger.info(`Graph layout finished. Generated ${nodes.length} nodes and ${edges.length} edges.`);
  return { nodes, edges };
}

function createDagreGraph(): dagre.graphlib.Graph {
  const g = new dagre.graphlib.Graph();
  g.setGraph({ rankdir: "LR", nodesep: 20, ranksep: 50, marginx: 50, marginy: 50 });
  g.setDefaultEdgeLabel(() => ({}));
  return g;
}

function registerNodes(g: dagre.graphlib.Graph, input: GraphInput): void {
  const { clients, hosts, dockNodes, containers } = input;
  logger.info("Registering nodes...");

  const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
    logger.debug(`Registering node in Dagre: [${type}] ${id}`);
    g.setNode(id, { width: 250, height: 80 });
  };

  for (const c of clients) addNodeToGraph(getClientId(c.id), "Client");
  for (const h of hosts) addNodeToGraph(getHostId(h.clientId, h.id), "Host");
  for (const co of containers) addNodeToGraph(getContainerId(co.id), "Container");
  for (const d of dockNodes) addNodeToGraph(getDockNodeId(d.id), "DockNode");
}

function registerEdgesForLayout(g: dagre.graphlib.Graph, input: GraphInput): void {
  const { hosts, containers } = input;
  logger.info("Registering edges for layout calculation...");

  hosts.forEach((host) => {
    const source = getClientId(host.clientId);
    const target = getHostId(host.clientId, host.id);
    g.setEdge(source, target);
  });

  containers.forEach((container) => {
    const source = getHostId(container.clientId, container.hostId);
    const target = getContainerId(container.id);

    if (g.hasNode(source)) {
      g.setEdge(source, target);
    } else {
      logger.warn(
        `Container ${container.id} references non-existent Host ${source}. Edge skipped.`
      );
    }
  });
}

2. Factor out repeated Dagre-node → FlowNode mapping

The logic for clients/hosts/containers/dockNodes shares a pattern: resolve Dagre node, guard, build FlowNode. You can use a small helper that centralizes the guard and position calculation:

function buildFlowNode(
  g: dagre.graphlib.Graph,
  nodeId: string,
  type: FlowNode["type"],
  data: FlowNode["data"]
): FlowNode | null {
  const dagreNode = g.node(nodeId);
  if (!dagreNode) {
    logger.error(`Missing node in Dagre output: ${nodeId}`);
    return null;
  }
  return {
    id: nodeId,
    type,
    position: calculatePosition(dagreNode),
    data,
  };
}

Then buildFlowGraphFromDagre can stay concise:

function buildFlowGraphFromDagre(
  g: dagre.graphlib.Graph,
  input: GraphInput
): { nodes: FlowNode[]; edges: FlowEdge[] } {
  const { clients, hosts, containers, dockNodes } = input;
  const nodes: FlowNode[] = [];
  const edges: FlowEdge[] = [];

  clients.forEach((client) => {
    const flowNode = buildFlowNode(g, getClientId(client.id), "client", {
      label: client.name || `Client ${client.id}`,
      status: client.initialized ? "online" : "offline",
      clientId: client.id,
    });
    if (flowNode) nodes.push(flowNode);
  });

  hosts.forEach((host) => {
    const hostId = getHostId(host.clientId, host.id);
    const flowNode = buildFlowNode(g, hostId, "host", {
      label: host.name || `Host ${host.id}`,
      status: host.reachable ? "online" : "offline",
      ipAddress: host.host,
      port: host.port,
      clientId: host.clientId,
      hostId: host.id,
    });
    if (flowNode) nodes.push(flowNode);

    edges.push(
      createEdge(
        getClientId(host.clientId),
        hostId,
        host.reachable,
        getEdgeStrokeColor(host.reachable)
      )
    );
  });

  // containers & dockNodes follow same pattern, using buildFlowNode
  // ...
  return { nodes, edges };
}

This removes the repeated g.node(...) / null-guard / calculatePosition(...) boilerplate.

3. Gate verbose logging behind a flag

The per-node debug logs are useful but noisy. If BaseLogger doesn’t already support it, a simple flag can keep the main algorithm readable while preserving logs:

const logger = BaseLogger.spawn("GraphCalc");
const DEBUG_GRAPH_LAYOUT = process.env.DEBUG_GRAPH_LAYOUT === "true";

function debugLog(message: string) {
  if (DEBUG_GRAPH_LAYOUT) {
    logger.debug(message);
  }
}

// usage in registerNodes:
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
  debugLog(`Registering node in Dagre: [${type}] ${id}`);
  g.setNode(id, { width: 250, height: 80 });
};

This keeps the key info/warn/error logs but hides the verbose details unless explicitly enabled.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an “Infrastructure Graph” feature spanning API → data/layout generation → frontend ReactFlow rendering, alongside a broader frontend hook refactor (moving Eden query/mutation usage to @dockstat/utils/react and app-local mutation wrappers) and some related API route cleanups.

Changes:

  • Introduce /api/v2/graph endpoint that assembles clients/hosts/containers/docknodes and generates positioned ReactFlow nodes/edges via Dagre.
  • Add ReactFlow-based graph page/UI (new /graph route) plus UI-library node components + theme variables for graph styling.
  • Refactor many frontend API calls to use eden.* hooks from @dockstat/utils/react and new mutation wrapper hooks; adjust Docker host/client routes to RESTful verbs/paths.

Reviewed changes

Copilot reviewed 100 out of 104 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
packages/ui/src/components/index.ts Re-export graph node components/types from UI package.
packages/ui/src/components/Sidebar/Sidebar.tsx Sidebar prop formatting + enable vertical scrolling on Card.
packages/ui/src/components/Navbar/consts.ts Add /dashboards and /graph items to sidebar paths.
packages/ui/src/components/Navbar/Navbar.tsx Adjust navbar layout + NavLink active matching (end).
packages/ui/src/components/Modal/Modal.tsx Allow modal title to be a ReactNode.
packages/ui/src/components/Link/Link.tsx Add end to NavLink for exact matching.
packages/ui/src/components/Graph/nodes/host.tsx New ReactFlow Host node UI.
packages/ui/src/components/Graph/nodes/docknode.tsx New ReactFlow DockNode node UI.
packages/ui/src/components/Graph/nodes/container.tsx New ReactFlow Container node UI.
packages/ui/src/components/Graph/nodes/client.tsx New ReactFlow Client node UI.
packages/ui/src/components/Graph/index.tsx Export node components + nodeTypes mapping for ReactFlow.
packages/ui/src/components/Forms/Selection.tsx Add rawOnChange option to Select component.
packages/ui/src/components/Forms/Input.tsx Convert Input component from React.FC to function + remove React import.
packages/ui/src/components/Card/Card.tsx Add custom card variant option.
packages/ui/src/App.css Add CSS variables for graph node styling.
packages/ui/package.json Add ReactFlow + cn dependency for UI package.
packages/typings/src/typebox/db.ts Add defaultDashboard to config schema.
packages/typings/src/docker-client.ts Add clientId to ContainerInfo.
packages/sqlite-wrapper/src/index.ts Type tightening + formatting improvements + better error throw formatting.
packages/sqlite-wrapper/package.json Bump sqlite-wrapper version.
packages/docker-client/src/shared/types.ts Add pingHost worker request type.
packages/docker-client/src/manager/utils/sendWorkerMessage.ts Resolve timeouts/crashes into WorkerResponse instead of rejecting; add error listener.
packages/docker-client/src/manager/hosts.ts Include host/port in mapped host output.
packages/docker-client/src/manager/core.ts Adjust sendWorkerMessage typing + ping behavior + host type expansion.
packages/docker-client/src/client/utils/mapContainerInfo.ts Include clientId in mapped container info.
packages/docker-client/src/client/mixins/monitoring/index.ts Pass clientId into monitoring manager.
packages/docker-client/src/client/mixins/hosts/index.ts Add pingHost method; reformat thrown Errors.
packages/docker-client/src/client/mixins/core/base.ts Pass clientId into monitoring manager.
packages/docker-client/src/client/mixins/containers/index.ts Thread clientId through container mapping + formatting.
packages/docker-client/src/client/managers/monitoring/monitors/ContainerMetrics.ts Track clientId; include in mapped container info; formatting.
packages/docker-client/src/client/managers/monitoring/monitors/ContainerEvents.ts Track clientId; include in mapped container info.
packages/docker-client/src/client/managers/monitoring/index.ts Plumb clientId into monitoring subcomponents.
packages/docker-client/src/client/managers/monitoring/eventStreamMonitor.ts Plumb clientId into container inspect mapping.
packages/docker-client/src/client/managers/host-handler/index.ts Use select([\"*\"]) instead of all() for hosts.
packages/docker-client/src/_worker.index.ts Add pingHost handling + formatting.
packages/db/defaults.ts Add defaultDashboard default config entry.
package.json Add new dev script, bump Turbo, add React DOM to catalog, bump type deps.
apps/dockstat/vite.config.ts Set publicDir for Vite build.
apps/dockstat/src/router.tsx Register /graph route.
apps/dockstat/src/providers/themeSidebar.tsx Switch to centralized theme mutations hook.
apps/dockstat/src/providers/theme.tsx Switch queries to eden.useEdenQuery; wrap theme create mutation behavior.
apps/dockstat/src/providers/additionalSettings.tsx Switch to eden.useEdenQuery.
apps/dockstat/src/pages/node/index.tsx Switch to eden.useEdenQuery + docknode mutation wrappers.
apps/dockstat/src/pages/index.tsx Replace Home content with placeholder.
apps/dockstat/src/pages/graph/index.tsx New graph page hosting ReactFlow provider + GraphFlow.
apps/dockstat/src/pages/extensions/plugins.tsx Switch to eden.useEdenQuery + plugin mutation wrappers.
apps/dockstat/src/pages/extensions/index.tsx Switch to eden.useEdenQuery + repo mutation wrapper.
apps/dockstat/src/pages/clients/index.tsx Switch to eden.useEdenQuery.
apps/dockstat/src/pages/clients/configure.tsx Switch to eden.useEdenQuery; adjust AddHost registered client shape.
apps/dockstat/src/layout/index.tsx Inline Layout implementation into layout index barrel.
apps/dockstat/src/layout/hooks/useThemeManager.ts Cleanup effect deps; add useRef import.
apps/dockstat/src/layout/hooks/usePluginRoutes.ts Switch to eden.useEdenQuery.
apps/dockstat/src/layout/hooks/usePinMutations.ts Switch to centralized config mutations hook.
apps/dockstat/src/layout/hooks/useLogs.tsx Adjust toast title rendering; log errors to console.
apps/dockstat/src/layout/hooks/useDeleteTheme.ts Switch to centralized theme mutations hook.
apps/dockstat/src/layout/components/Layout.tsx Remove old Layout component (moved to layout index).
apps/dockstat/src/index.css Add ReactFlow attribution styling + add Geist font faces + theme font vars.
apps/dockstat/src/hooks/useEdenQuery.ts Remove old local Eden query helper (migrated to utils).
apps/dockstat/src/hooks/plugins/usePluginTemplate.ts Switch to plugin template mutation wrapper.
apps/dockstat/src/hooks/plugins/usePluginLoaders.ts Switch to eden.useEdenRouteMutation.
apps/dockstat/src/hooks/plugins/usePluginActions.ts Switch to eden.useEdenRouteMutation.
apps/dockstat/src/hooks/mutations/theme.ts New theme mutation wrappers.
apps/dockstat/src/hooks/mutations/repo.ts New repo mutation wrappers.
apps/dockstat/src/hooks/mutations/plugin.ts New plugin mutation wrappers.
apps/dockstat/src/hooks/mutations/node.ts New DockNode mutation wrappers.
apps/dockstat/src/hooks/mutations/index.ts Barrel export for mutation wrappers.
apps/dockstat/src/hooks/mutations/docker-host.ts New docker-host mutation wrappers.
apps/dockstat/src/hooks/mutations/docker-client.ts New docker-client mutation wrappers.
apps/dockstat/src/hooks/mutations/dashboard.ts New dashboard mutation wrappers (incl. default dashboard).
apps/dockstat/src/hooks/mutations/config.ts New config mutation wrappers (pin/unpin/settings/hotkeys).
apps/dockstat/src/hooks/eden/useEdenMutation.ts Add edenMutation alias export.
apps/dockstat/src/contexts/theme.ts Re-type theme mutation result using eden.MutationResult.
apps/dockstat/src/components/settings/hotkeys.tsx Switch to centralized config mutations hook.
apps/dockstat/src/components/settings/general/sections/useGeneralSettings.tsx Switch to eden.useEdenQuery + centralized config mutations.
apps/dockstat/src/components/graph/statsDisplay.tsx New graph stats pill display component.
apps/dockstat/src/components/graph/nodeDetails.tsx New selected-node details panel component.
apps/dockstat/src/components/graph/legend.tsx New legend sidebar component.
apps/dockstat/src/components/graph/flow.tsx New ReactFlow graph canvas + sidebar + data fetching.
apps/dockstat/src/components/graph/customEdge.tsx New labeled edge renderer using EdgeLabelRenderer.
apps/dockstat/src/components/extensions/RepoCard.tsx Switch to repo delete mutation wrapper.
apps/dockstat/src/components/clients/configure/AddHost.tsx Switch to docker-host mutation wrappers + richer client selection UI.
apps/dockstat/src/components/clients/configure/AddClient.tsx Switch to docker-client mutation wrapper.
apps/dockstat/public/fonts/GeistPixel-Square.woff2 Add Geist Pixel font asset.
apps/dockstat/package.json Add ReactFlow + Geist deps.
apps/api/src/routes/node/index.ts Use shared DockNodeHandler instance; formatting.
apps/api/src/routes/graph/index.ts New /graph API route returning nodes/edges + entity lists.
apps/api/src/routes/docker/hosts.ts Convert host routes to RESTful / + patch/delete verbs.
apps/api/src/routes/docker/client.ts Convert client routes to RESTful / + patch/delete verbs.
apps/api/src/routes/db.ts Add config endpoint to set/clear defaultDashboard.
apps/api/src/models/graph.ts New response schemas for graph endpoint.
apps/api/src/models/database.ts Expand additional settings response schema; formatting.
apps/api/src/middleware/metrics/helper.ts Make header access optional-safe.
apps/api/src/index.ts Register Graph routes.
apps/api/src/handlers/requestLogger.ts Make header access optional-safe.
apps/api/src/graph/types.ts New types for graph layout input/output.
apps/api/src/graph/reachableStatus.ts Map docknode reachability to OK/NO/not-initialised.
apps/api/src/graph/index.ts Export graph calculator + types.
apps/api/src/graph/helper.ts Shared ID/styling helpers for graph generation.
apps/api/src/graph/graphCalculator.ts Dagre-based graph layout + ReactFlow node/edge generation.
apps/api/src/docker/docknode.ts Centralize DockNodeHandler instance (DNH).
apps/api/package.json Add dagre + typings; reorder deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Its4Nik and others added 3 commits March 17, 2026 22:20
This commit applies several updates across the API and frontend to improve data consistency and user interface feedback:

- Updated GraphModel schema in the API to include new optional fields: dockNodeId, state, containerId, label, type, and edgesReconnectable.
- Improved null safety in AddHost component when accessing registered client IDs.
- Enhanced data validation logic in GraphFlow to correctly handle success flags and empty node arrays.
- Fixed a typo in the log toast notification and corrected the success message when unpinning items in the layout.
- Cleaned up TypeScript event handler definitions in the Selection UI component.
- Removed a redundant ping request within the DockerClientManagerCore loop to optimize performance.
…in GraphFlow [N/A]

Removed a redundant 'as any' type assertion when checking the nodes array in the GraphFlow component. Additionally, corrected the indentation of the hasNoData constant to align with project coding standards.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants