Skip to content

Commit 541bff1

Browse files
agentofrealityCopilotAllen Jones
authored
Enhance Drasi Server with Web UI, solutions, and dynamic loading of pluggins. (#84)
* feat: Add Drasi Control UI with real-time SSE updates - React 19 + TypeScript + Vite + Tailwind gaming-inspired management UI - Flow canvas with React Flow showing Sources → Queries → Reactions topology - Full CRUD: create/start/stop/delete for sources, queries, reactions, instances - Draft-based editing (local state until explicit save) - Per-kind config forms for all source types (mock, HTTP, gRPC, Postgres, platform), query config, and all reaction types (log, HTTP, gRPC, SSE, profiler) - Instance selector with create dialog for multi-instance management - Inspector panel with status, config details, connections, and action buttons - Real-time updates via SSE instead of polling (/api/v1/events endpoint) - Internal components (__introspection__, __attach_*) filtered from UI - Fix: Query status now reflects actual state instead of hardcoded 'Running' - Server: Added global component events SSE endpoint for reactive UI updates - Server: Added ComponentStatus Added/Removed variants to observability DTOs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add source data push proxy, expandable canvas nodes, and canvas persistence Backend: - Add POST /api/v1/sources/:id/push proxy endpoint to forward data to HTTP/gRPC source listening ports, bypassing browser CORS restrictions - Add instance-specific and default-instance route handlers for the new push endpoint in v1/handlers.rs and v1/routes.rs - Add reqwest 0.12 dependency for outbound HTTP client calls - Fix clippy uninlined_format_args lint errors in shared/handlers.rs UI Canvas: - Add NodeShell component as a shared wrapper for all node types with expand/collapse animation (framer-motion), per-node locking, and consistent handle rendering - Refactor SourceNode, QueryNode, and ReactionNode to use NodeShell, exposing detailed config (query text, source lists, reaction properties) in the expanded view - Add SourcePushPanel component for inline test data submission to HTTP/gRPC sources via the new push proxy endpoint - Add useAutoLayout hook with collision-aware node clamping to prevent expanded nodes from overlapping neighbors - Add useCanvasPersistence hook to save/restore node positions, expanded state, lock state, and viewport to localStorage per instance - Add canvas-level lock toggle, multi-select node deletion with confirmation, and Delete/Backspace keyboard shortcut support - Update CSS: edges render above nodes (z-index), smooth position transitions for displaced nodes, refined node-card transition props UI Instance Management: - Support ?instance= URL search param for deep-linking to instances - Persist selected instance to localStorage across sessions - Show 'instance not found' banner with option to create the missing instance, pre-filling the ID in CreateInstanceDialog UI Data Model: - Add properties field to SourceStatusResponse and ReactionStatusResponse - Add query and queryLanguage fields to pipeline QueryInfo - Export layout constants (COLUMN_X, NODE_SPACING_Y, NODE_START_Y) from graph utils for use by auto-layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix SSE connection exhaustion causing slow UI interactions The Create Instance dialog (and other API calls) would hang for a long time because the browser's HTTP/1.1 connection pool was saturated by SSE streams. Root cause: useSources, useQueries, and useReactions each opened their own EventSource to the same /events endpoint, consuming 3 of the browser's 6 concurrent connections per domain. With inspector panels adding per-component SSE streams on top, new API requests (POST, GET) would queue indefinitely behind the persistent SSE connections. Changes: 1. Shared EventSource singleton (ui/src/hooks/useApi.ts): - All hooks now share a single EventSource per instance via a module-level Map<instanceId, SharedES> that multiplexes events to multiple listeners - When the last listener unsubscribes, the EventSource is closed and removed - Reduces SSE connections from 3 per page to 1 per instance 2. Create Instance dialog loading state (CreateInstanceDialog.tsx): - handleSave is now async and awaits the onSave callback - Added saving state with 'Creating...' button text and disabled state - onSave prop typed as Promise<void> for proper async handling - Errors during creation are caught and shown inline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix nodes visibly shrinking on initial page load Nodes would mount at a default width then animate to their persisted size, causing a jarring resize flash when the UI first loaded. Root cause: buildFlowGraph() created nodes without expanded/locked/position state. useCanvasPersistence restored this state in a useEffect AFTER mount, triggering a Framer Motion width animation from default → persisted size. Fix: 1. FlowCanvas now reads persisted state from localStorage synchronously during useMemo (before mount) and pre-applies positions, expanded, and locked flags to the initial nodes. 2. NodeShell sets initial={{ width: targetWidth }} on the motion.div so Framer Motion starts at the correct width — no animation on mount. Subsequent expand/collapse interactions still animate normally. 3. Exported loadPersistedState() from useCanvasPersistence for reuse. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Drasi logo to Admin UI and rename from 'Drasi Control' to 'Drasi Server' - Create DrasiLogo.tsx component with the official Drasi chevron icon SVG (lime #c3fb3b + green #48e263 arrow polygons) and full wordmark variant - Replace placeholder gradient 'D' square in AppLayout header with DrasiLogo - Rename 'DRASI CONTROL' to 'DRASI SERVER' in the UI header - Update HTML page title from 'Drasi Control' to 'Drasi Server' - Update server.rs log messages from 'Drasi Control UI' to 'Drasi Server Admin UI' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reorganize canvas lock controls and add multi-select node lock toggle - Move global canvas lock from top-right panel into bottom-left React Flow Controls toolbar as a ControlButton for consistent placement - Hide React Flow's built-in interactivity toggle (showInteractive=false) to avoid duplicate lock icons in the controls panel - Add selection-aware lock/unlock button in top-right toolbar that appears alongside delete when nodes are selected (locks all selected if any are unlocked, unlocks all if all are locked) - Top-right panel now only renders when canvas is unlocked and nodes are selected, keeping the canvas clean when not in multi-select mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Replace bottom event bar with slide-out activity panel and update favicon - Replace EventBar (fixed bottom bar) with EventPanel (left slide-out panel) that shows all events as a scrollable list with colored status dots - Wire the Activity icon in the header as a toggle button to open/close the panel, with a badge showing unread event count - Add Clear and Close buttons to the panel header - Remove .event-bar CSS class (no longer needed) - Rename EventBar.tsx to EventPanel.tsx to match new component name - Replace placeholder favicon with Drasi chevron logo (lime + green arrows on dark background), properly centered in 32x32 viewBox Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): add light/dark theme toggle with localStorage persistence Introduce a complete theming system that allows users to switch between light and dark modes, with their preference remembered across sessions. CSS variable-based theming: - Define light theme variables on :root and dark theme variables on .dark - Replace all hardcoded hex colors in tailwind.config.js with var(--drasi-*) - Enable Tailwind darkMode: "class" strategy - Add --drasi-minimap-mask variable for theme-aware minimap overlay useTheme hook (new file): - Reads/writes theme preference to localStorage under drasi-theme key - Toggles .dark class on <html> element to switch CSS variable sets - Defaults to dark theme when no preference is stored Flash-of-unstyled-content prevention: - Inline script in index.html applies saved theme before first paint - Removes hardcoded class="dark" from <html>, letting the script decide Header theme toggle: - Sun icon (in dark mode) / Moon icon (in light mode) in AppLayout header - Theme state and toggle callback threaded from App.tsx through props Theme-aware component updates: - DrasiLogo wordmark fill changed from #fff to currentColor - FlowCanvas Background and MiniMap use CSS variables instead of hex - NodeShell hover states use drasi-text-secondary/10 instead of white/10 - Edge strokes in graph.ts use var(--drasi-border) for inactive edges - colors.ts adds getTheme() that reads computed CSS variables at runtime Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix smooth vertical displacement of nodes during expand/collapse Apply vertical displacement in the same setNodes call as the expand toggle so both CSS transitions start in the same paint frame. Add height target locking in useAutoLayout to prevent intermediate ResizeObserver measurements from restarting CSS transitions. Switch to useLayoutEffect for displacement to commit before paint. Slow height expansion from 400ms to 405ms to prevent the expanding node from visually outrunning displaced neighbors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add solution templates feature with catalog UI and validation-first deployment Features: - Solution templates system with YAML-based templates in /solutions directory - REST API endpoints: GET/POST /api/v1/catalog/solutions for browsing, POST /api/v1/instances/{id}/solutions for deployment - Variable substitution with ${VAR:-default} syntax and YAML comment extraction - UI integration: Browse Catalog tab in Add dialog, deploy dialog with instance creation, variable configuration showing descriptions and usedBy Deployment improvements: - Validation-first: parse and validate ALL components before creating any - Returns all validation errors at once so users can fix them together - Creation order: sources → queries → reactions (all stopped initially) - Start phase only after all creations succeed UI/UX improvements: - Redesigned Add dialog with tabbed interface (Add Component / Browse Catalog) - Redesigned deploy dialog with visual component breakdown - Error messages displayed on nodes and inspector panel - Instance selector with inline create option in deploy dialog - Fixed crypto.randomUUID fallback for older browsers - Smarter error handling (only clear lists on 404, not transient errors) Other: - Added dev-build and clean-dev-build Makefile targets - Updated OpenAPI documentation with solution endpoints - DtoMapper now supports variable overrides for template instantiation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): standardize collapsed node heights across all component types Add collapsedMinHeight prop to NodeShell component and set it to 85px for all node types (Source, Query, Reaction) to ensure uniform sizing when collapsed on the canvas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): animate edges only when both connected nodes are running Previously, source→query edges animated based on query status only, and query→reaction edges animated based on reaction status only. Now edges only animate (and show green) when BOTH endpoints are running. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add live query results to UI and fix solution template label UI changes: - Add useQueryResults hook for fetching and streaming query results via SSE - Display live results table in expanded QueryNode with streaming indicator - Pass instanceId through component graph for proper API routing Solution template fix: - Fix iot-temperature-monitor.yaml query label: Sensor -> sensorReading (mock source generates SensorReading nodes, not Sensor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add comprehensive solution template tests with E2E data flow validation Add 47 new tests for solution template functionality: - solution_catalog_test.rs (15 tests): Template listing, details, variable extraction, and catalog API validation - solution_deployment_test.rs (22 tests): Solution deployment via API, variable substitution, validation errors, multi-instance support, scriptfile bootstrap, and query result validation - solution_e2e_data_flow_test.rs (10 tests): Complete pipeline validation using wiremock to capture HTTP reaction output and verify data flows correctly from Source → Query → Reaction with payload validation Test infrastructure: - tests/test_support/solution_helpers.rs: Template generators, router setup, wait helpers for query results, JSONL file creation utilities - tests/fixtures/solutions/: Test fixture templates Also fixes flaky log stream tests in api_integration_test.rs by using unique instance IDs per test to avoid global log registry interference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve light theme contrast and fix IoT template query label Light theme UI improvements: - Darken canvas background (#f8fafc → #e2e8f0) so white cards stand out - Use pure white (#ffffff) for node cards with subtle drop shadow - Increase border contrast (#cbd5e1 → #94a3b8) for visible card edges - Add --drasi-edge variable (#64748b) for darker connection lines - Improve secondary text contrast (#64748b → #475569) Fix iot-temperature-monitor.yaml: - Change query label from 'sensorReading' to 'SensorReading' (PascalCase) to match the labels generated by MockSource Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Redesign node cards with compact toolbar and improved UX - Move lock/start-stop/expand controls to bottom toolbar on right - Change lock icon to pin with angled rotation for unpinned state - Reduce node card padding for tighter layout (p-3 → px-2 py-1.5) - Increase toolbar icon size to 14px for better visibility - Keep expand button always visible (greyed out when disabled) - Add canvas click handler to close inspector panel - Add slide-out animation for inspector panel using Framer Motion - Add useApi hook for direct start/stop API calls from nodes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Redesign inspector panels and canvas controls - Add specialized inspector panels for Source, Query, and Reaction components - Each panel shows component-specific config and Data Flow section - Data Flow shows INPUT sources and OUTPUT reactions/queries - Redesign node toolbar with crossfade animation between collapsed/expanded states - Pin and expand controls in bottom toolbar (collapsed) or top-right (expanded) - Controls disabled (not hidden) when canvas is locked - Add delete confirmation dialog with dependency checking - Sources cannot be deleted if queries depend on them - Queries cannot be deleted if reactions depend on them - Inspector panel slides out with animation on close - Expanded nodes show only runtime info (results/activity), config in inspector only - Canvas controls sized to 32x32 with 18px icons Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Update component color scheme to Drasi conventions Standardize colors across all UI components: - Sources: Green (#22c55e) - Queries: Blue (#3b82f6) - Reactions: Purple (#8b5cf6) Centralize color definitions in tailwind.config.js and utils/colors.ts. Replace all hardcoded color values with semantic Tailwind classes (text-drasi-source, text-drasi-query, text-drasi-reaction, etc.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Match inspector panel icons to canvas node icons - SourceInspectorPanel: Use kind-based icon map (Database, Globe, Radio, etc.) - QueryInspectorPanel: Use Search icon (consistent with QueryNode) - ReactionInspectorPanel: Use kind-based icon map (FileText, Globe, Rss, etc.) Icons now match between node cards and their inspector panels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add .vscode to .gitignore Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove .vscode from git tracking Files were already tracked before adding to .gitignore. Use 'git rm --cached' to untrack while keeping files on disk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Clean up persisted node state on deletion - Add removeNodeFromPersistedState() to clean up localStorage when a node is deleted - Add removeInstancePersistedState() for future instance deletion support - Update useSources/useQueries/useReactions remove() to clean up persisted state Node positions are already partitioned by instance ID via storage key prefix. Auto-save on node add is handled by the fingerprint change detection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Show query language type on query nodes Display 'GQL Query' or 'Cypher Query' instead of generic 'Continuous Query' based on the queryLanguage property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Add auto-layout button to canvas controls New button arranges nodes in columns by type: - Sources on left (x=50) - Queries in center (x=400) - Reactions on right (x=750) Uses LayoutGrid icon from lucide-react. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Widen instance selector for GUID display - Increase dropdown width from 256px to 384px (w-64 → w-96) - Increase display name truncation from 16 to 32 characters GUIDs are 36 characters, so most instance IDs will now display fully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UI: Fit view after auto-layout Auto-layout now centers and zooms to show all nodes after rearranging. Uses React Flow's fitView with 20% padding and 300ms animation. Refactored AutoLayout component to expose canvas API (collision + fitView) via ref pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add instance management features (clone, create with template, export template) Add three new instance management capabilities to the Drasi Server UI: 1. Create Instance with Solution Template - Add solution template dropdown to CreateInstanceDialog - Auto-deploy selected template after instance creation - New instance becomes active after completion 2. Clone Instance - Add CloneInstanceDialog with progress tracking - Clone menu item in InstanceSelector - Copies all sources, queries, reactions with autoStart=false - Client-side orchestration using existing CRUD endpoints 3. Create Solution Template from Instance - Add CreateSolutionTemplateDialog with component selection - Users can select which sources/queries/reactions to include - New server endpoint: POST /api/v1/instances/{id}/catalog/solutions - Exports selected components as reusable YAML template Backend changes: - Add CreateSolutionTemplateRequest/Response types - Implement create_solution_template in solutions.rs - Add handler and route for new endpoint - Update OpenAPI documentation with new endpoint and schemas - Add unit tests for request validation and response types UI changes: - New CloneInstanceDialog.tsx component - New CreateSolutionTemplateDialog.tsx component - Update InstanceSelector with clone/template menu items - Update CreateInstanceDialog with template selector - Add createSolutionTemplate to API client Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: Add instanceId to reaction node data and fix clippy warnings - Add missing instanceId to reaction pipeline data in App.tsx This fixes the reaction node toolbar start/stop buttons not working because instanceId was undefined when calling the API - Fix clippy errors in solutions.rs by using inline format string variables (e.g., {source_id} instead of "{}", source_id) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(ui): Improve Query inspector panel layout - Remove redundant Quick Stats section (sources/lines/reactions boxes) since this info is already shown in the Data Flow section - Increase query definition box max height from 256px to 384px - Use explicit vertical scrollbar (overflow-y-auto) for long queries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): Add interactive controls to inspector Data Flow sections Add start/stop buttons and clickable navigation to connected components in all three inspector panels (Source, Query, Reaction). New features: - ConnectedComponentItem: Reusable component for data flow items - Clickable component names navigate to that component's inspector - Start/Stop buttons for each connected component based on status - Spinning indicator shown during Starting/Stopping transitions Changes: - Create ConnectedComponentItem.tsx shared component - Update SourceInspectorPanel with onNavigate, onStartQuery, onStopQuery - Update QueryInspectorPanel with onNavigate, onStart/Stop for sources and reactions - Update ReactionInspectorPanel with onNavigate, onStartQuery, onStopQuery - Wire up all handlers in App.tsx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: make web UI optional via config and CLI flags Add enableUi config property and --enable-ui/--disable-ui CLI flags to control whether the web UI is served at /ui. Changes: - Add enable_ui field to DrasiServerConfig (default: true) - Add enable_ui to ResolvedServerSettings and DrasiServer structs - Add --enable-ui and --disable-ui CLI flags (mutually exclusive) - CLI flags override config file setting - Update start_api() to conditionally register UI routes - Add enable_ui()/disable_ui() methods to DrasiServerBuilder - Update config examples with enableUi documentation When UI is disabled: - /ui routes are not registered (returns 404) - Root / redirect to /ui/ is not registered - Log message indicates UI is disabled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: Comprehensive README rewrite for newcomers Significantly expanded README.md from ~1,200 to ~2,500 lines to provide a complete user guide for Drasi Server. New sections added: - Table of Contents for easy navigation - Detailed Quick Start tutorial with step-by-step instructions - Web UI Guide covering Flow Canvas, Inspector Panels, Activity feed - Instances section explaining multi-instance concepts - Solution Templates documentation including deployment and creation - VS Code Extension installation and usage guide - Development Utilities (Makefile commands reference) - Complete Configuration Examples with 4 real-world scenarios Enhanced sections: - REST API with curl examples for all endpoints, SSE streaming - CLI Reference with --enable-ui/--disable-ui flags, .env support - Troubleshooting expanded to 10+ common issues with solutions - Docker deployment with better quick-start commands The documentation is now targeted at users with little or no Drasi experience while remaining useful as a technical reference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct temperature threshold in IoT solution template The IoT temperature monitor solution template had a default TEMP_THRESHOLD of 75°C, but MockSource generates temperatures in the 20-30°C range. This meant no data ever matched the query filter. Changes: - Fix default threshold from 75 to 25 in iot-temperature-monitor.yaml - Fix same issue in test fixtures and helpers - Add comprehensive e2e tests proving data flows through the pipeline - Fix README examples using env vars on boolean/enum fields (not supported) - Update solution_catalog_test to expect new threshold value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): use consistent colors for start/stop buttons in DATA FLOW section Update ConnectedComponentItem to use semantic drasi-running and drasi-error colors instead of hardcoded green-500 and amber-500, matching the button styling used on nodes and in ActionButtons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): auto-arrange uses actual node dimensions for proper spacing - Use measured widths from nodeLookup instead of hardcoded TARGET_WIDTHS - Calculate column X positions dynamically based on actual expanded node sizes - Use measured heights for proper Y spacing between nodes - Increase node margin to 80px for better visual separation - Maintains Sources → Queries → Reactions column ordering (left to right) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): Add SolutionInstanceWizard for template-based instance creation - Create new SolutionInstanceWizard component with multi-step wizard flow for selecting solution templates and configuring new instances - Add 'Create from Solution Template' option to InstanceSelector dropdown - Simplify CreateInstanceDialog by removing inline template selection (templates are now handled by the dedicated wizard) - Update App.tsx to integrate the new wizard with proper state management - Fix query return clauses in solution templates to return specific fields instead of entire nodes (iot-temperature-monitor, simple-log-pipeline) - Remove redundant component refresh calls after deploy (hooks auto-refresh) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): Add sector performance aggregation with real-time updates - Add SectorPerformance component displaying aggregated sector metrics - Create sector-performance-query with GROUP BY aggregation (count, avg, sum, min, max) - Fix SSE client to properly extract aggregation results from SSE reaction - Fix API handlers to use ComponentGraph as source of truth for reactions/sources - Add row highlight animation when sector values change - Sort sectors alphabetically and align component height with other panels - Update useDrasi hook with debug logging for sector query - Add Drasi Server UI URL to start-demo.sh output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): Make watchlist data-driven with three-way join - Add watchlist table to PostgreSQL schema with CDC support - Create ON_WATCHLIST synthetic join (watchlist → stocks → stock_prices) - Update watchlist query to use join instead of hardcoded WHERE clause - Change UI sorting from hardcoded order to alphabetical by symbol - Populate watchlist with META, AMZN, AMD (stocks not in portfolio) - Add watchlist to server config tables and replication publication This demonstrates Drasi's multi-table join capability with real-time updates flowing through the entire join chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): Add CRUD UI for watchlist and portfolio management - Add Flask REST API (trading_api.py) for database CRUD operations - GET/POST/DELETE for watchlist items - GET/POST/PUT/DELETE for portfolio positions - GET for available stocks list - Add TradingApi.ts TypeScript service for frontend API calls - Create new Watchlist.tsx component with: - Add button with stock selector modal - Remove button per row - Real-time price updates via Drasi SSE - Enhance Portfolio.tsx with: - Add Position button with quantity/price inputs - Edit icon per row with modal for updates - Delete icon with confirmation dialog - Price change highlight animations - Fix SSE DELETE handling: - SSEClient now processes DELETE results and marks items with _deleted flag - useDrasi removes deleted items from data map - Update start-demo.sh to launch Trading API on port 9200 - Update stop-demo.sh to clean up Trading API process This demonstrates end-to-end reactive data flow: UI Action → Database Write → PostgreSQL CDC → Drasi Query → SSE → UI Update Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(trading): Extract queries to separate presentation-friendly file - Create queries.ts with all Cypher query definitions - Document synthetic joins (HAS_PRICE, OWNS_STOCK, ON_WATCHLIST) - Add descriptions explaining what each query demonstrates - Export ALL_QUERIES array and QUERIES_BY_ID map for easy access - Update DrasiClient.ts to import from queries.ts This makes queries easy to show during presentations without digging through class implementation code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): refactor position management with new dialog component - Extract PositionDialog as reusable component for add/edit operations - Add purchase date support to positions (TradingApi, types, forms) - Improve delete confirmation with full position details - Simplify Portfolio.tsx state management (unified dialog mode) - Enhance SSE client with better reconnection handling - Update mock generator with purchase date support - Consolidate form error handling within dialog component Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(trading): Extract reusable QueryTable component and shared utilities Introduce QueryTable - a generic, sortable table component that subscribes to Drasi queries and handles data display, sorting, animations, and row actions. This consolidates ~500 lines of repeated table logic across Portfolio, Watchlist, StockList, and SectorPerformance components. New shared modules: - QueryTable.tsx: Reusable table with column definitions, sorting, row actions, and change animations - PortfolioSummary.tsx: Dedicated component for portfolio stats display - useRowAnimation.ts: Hook for tracking value changes with directional animations (price up/down indicators) - formatters.ts: Shared number formatting (currency, percent, volume) Component updates: - Portfolio: Now uses QueryTable with PositionDialog for CRUD operations - Watchlist: Simplified to QueryTable with inline add/remove actions - StockList: Reduced to thin wrapper around QueryTable - SectorPerformance: Converted to declarative column definitions Net reduction of ~475 lines while adding new features (sorting, animations). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(trading): Extract shared UI components to reduce duplication Create a shared component library for the trading app that consolidates repeated patterns across multiple components: New shared components: - Icons.tsx: Reusable icon components (ArrowUp/Down, Edit, Delete, Add, etc.) - ChangeIndicator.tsx: Percentage change display with directional arrows - BaseDialog.tsx: Base modal with escape-to-close and overlay-click handling - ConfirmDialog.tsx: Destructive action confirmation with detail section - SelectDialog.tsx: Simple select-and-confirm dialog pattern Component updates: - Portfolio: Uses ConfirmDialog for delete confirmation, shared icons - Watchlist: Uses SelectDialog for add-to-watchlist, shared icons - PositionDialog: Refactored to use BaseDialog for consistent modal behavior - StockList, SectorPerformance: Use shared ChangeIndicator Benefits: - ~186 lines removed from existing components - Consistent modal behavior (escape key, body scroll lock) - Single source of truth for icons and change indicators - Easier to maintain and extend Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading-ui): Reorganize dashboard panel layout - Add PlaceholderTable component for Orders panel - Rearrange grid layout into three distinct rows: - Row 1: Watchlist (1/3) + Portfolio (2/3) - Row 2: Sector Performance + Orders (equal 50% width each) - Row 3: Top Gainers + Top Losers + High Volume (equal 1/3 each) - Use separate grid containers per row for better control Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading-ui): Add code viewer dialog for presentations Add a code viewer feature to QueryTable components that displays: - React component code snippets (simplified for presentation clarity) - Cypher queries (live from queries.ts registry) New components: - CodeViewerDialog: Tabbed dialog with copy-to-clipboard, large text - CodeIcon: </> brackets icon for triggering the dialog Features: - Animated slide-up entrance with fade overlay - Lighter dialog background for better contrast - Two tabs: 'React Code' and 'Cypher Query' - Code icon appears in table header when codeSnippet prop provided Updated components with code snippets: - Portfolio, Watchlist, SectorPerformance, StockList Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(trading): only delete SSE reaction if app created it Add consistent lifecycle management between queries and reactions in the trading app's DrasiClient: - Add createdReaction flag to track if this session created the reaction - Set flag when creating new reaction (404 case) - Only delete reaction on cleanup if this session created it - Reset flag after cleanup This allows multiple browser sessions to safely share one SSE reaction - only the session that originally created it will delete it on close. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): Add limit orders with Drasi future function demo Implement limit orders feature demonstrating drasi.trueFor() temporal queries: - Add limit_orders table with status lifecycle (pending → stale → expired) - Create postgres-broker source with separate replication slot - Add stale orders query (drasi.trueFor 15s pending detection) - Add expiring orders query (drasi.trueFor duration-based expiration) - Create Orders component with order entry dialog - Add useOrderStatusUpdates hook for automatic status transitions - Support string-based animations for status changes (blue flash) - Fix DrasiClient to store empty query results (prevents loading hang) - Fix QueryInspectorPanel query formatting - Fix CodeViewerDialog portal rendering to prevent layout shifts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading-app): fetch query definition from Drasi Server and add UI link Replace local QUERIES_BY_ID lookup in CodeViewerDialog with a live fetch from the Drasi Server API (GET /api/v1/queries/{id}?view=full). The Query Definition tab now displays the full query config including id, sources, joins, and metadata in a readable YAML-like format. Also adds an 'Open in Drasi UI' link in the dialog header that navigates to the Drasi Server UI with the correct instance pre-selected. Changes: - DrasiClient: add getQueryConfig() and getDrasiUiUrl(), discover instance ID during initialization - useDrasi: add useQueryDefinition() and useDrasiUiUrl() hooks - QueryTable: use server-fetched config with formatQueryConfig() helper - CodeViewerDialog: accept drasiUiUrl prop, render external link button, rename tab to 'Query Definition' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(trading-app): use camelCase Cypher aliases, remove runtime transform Cypher AS aliases are arbitrary strings, so use camelCase directly (e.g., AS changePercent, AS previousClose, AS profitLoss) to match the React/TypeScript conventions. This eliminates the snake_case to camelCase runtime transform in useDrasi.ts handleResult. Changes: - queries.ts: rename all 25+ snake_case AS aliases to camelCase across all 11 query definitions - DrasiClient.ts: update createCustomQuery alias to camelCase - useDrasi.ts: remove key.replace(/_([a-z])/g, ...) transform, keep portfolio numeric string parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(trading-app): portfolio summary not updating due to unstable map key The portfolio-summary-query returns a single aggregation row with no symbol or id field, so getItemKey fell back to JSON.stringify(item). Since values change on every price update, each update produced a new key, causing the map to accumulate stale entries. data?.[0] always returned the oldest (first) entry, making it appear frozen. Fix: return a stable key 'portfolio-summary' for this single-row query so the entry is updated in place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading-app): add expandable tables for presentations Add expand/collapse functionality to QueryTable for better readability during presentations. Click the expand button to animate the table from its position to near-fullscreen with enlarged text; click collapse or press Escape to reverse. Features: - FLIP-style animation: captures bounding rect, renders portal at original position, CSS-transitions to fullscreen with backdrop - Text scales up simultaneously with the container expansion using synchronized CSS transitions and expanded-table-text class - All child text (including hardcoded text-sm/text-xs in column formatters) inherits the enlarged font size via CSS override - Portfolio summary headerSlot also scales when expanded - Escape key and backdrop click to collapse Fixes: - Fixed Rules of Hooks violation (expandedStyle useMemo after early return) that caused black screen on data load - All dialogs (add to watchlist/portfolio, limit orders, code viewer) now use z-[100] so they render above expanded tables (z-60) - Removed 'Updated' timestamp from table headers, moved action buttons (code, expand) to the right side - Enlarged code viewer dialog text to text-2xl for readability New shared icons: ExpandIcon, CollapseIcon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(trading-app): wrap DECIMAL fields in toFloat() for CDC compatibility PostgreSQL CDC (WAL replication) serializes DECIMAL/NUMERIC columns as text strings. Cypher arithmetic on strings returns null, causing P/L and related computed fields to show as '-' for positions added via the API. Bootstrap-loaded data (init.sql) is unaffected because the SELECT driver returns proper numeric types. Wrap p.purchase_price in toFloat() in portfolio and portfolio-summary queries, and o.target_price in the active-orders distance calculation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): duration-based trueFor queries and order management fixes Replace trueLater with trueFor using pre-calculated duration columns to work around PostgreSQL CDC TIMESTAMP serialization incompatibility with Drasi's datetime() parser (space separator vs T, no timezone offset). Schema changes (init.sql): - Add stale_duration and expire_duration INTEGER columns to limit_orders - Revert TIMESTAMPTZ back to TIMESTAMP (TIMESTAMPTZ broke CDC events) Query changes (queries.ts): - Stale orders: trueFor(status='pending', duration({seconds: o.stale_duration})) - Expiring orders: trueFor(status='stale', duration({seconds: expire - stale})) UI changes: - OrderDialog computes staleDuration (floor(expiresIn/2)) and expireDuration - TradingApi.ts and Orders.tsx pass durations through to API - Delete button enabled for all order statuses - Stock ticker z-index lowered from 1000 to 40 (was covering dialogs) API changes (trading_api.py): - Accept and insert staleDuration/expireDuration fields - Remove status restriction on order deletion (any order can be deleted) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(trading): add scriptfile bootstrap for price-feed source Add initial-prices.jsonl with all 50 stocks at their base prices so queries have price data immediately on startup without waiting for the Python price generator. Uses the same node IDs (price_{SYMBOL}) and labels (stock_prices) as the generator so live updates seamlessly overwrite the bootstrap values. Configure the price-feed HTTP source with a scriptfile bootstrap provider pointing to the JSONL file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(trading-app): fix layout jitter and UI polish - Widen layout from max-w-7xl (1280px) to 1400px to give tables more room - Add min-w-0 to all grid cells to prevent content overflow in CSS Grid - Change table scroll container from overflow-auto to overflow-y-auto overflow-x-hidden to suppress horizontal scrollbar flicker - Truncate Watchlist Name column to prevent wrapping - Make Query Definition the default/first tab in code viewer dialog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Broker Panel UI and improve trading demo cleanup - Add broker.html: standalone Broker Panel UI for simulating order fills, expirations, and price updates against the trading API - Serve Broker Panel from /broker route in trading_api.py - Simplify DrasiClient cleanup to leave queries/reactions in place on disconnect instead of deleting them - Add sample expired orders to init.sql seed data - Include Broker Panel URL in start-demo.sh output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Cypher/GQL syntax highlighting to Query Inspector Replace plain <pre> query display with a read-only Monaco Editor that provides language-aware syntax highlighting for Cypher and GQL queries. The editor auto-sizes to content height using Monaco's getContentHeight() API, eliminating spurious scrollbars. - Add Monarch tokenizer definitions for Cypher and GQL languages with keywords, builtins, labels, parameters, strings, and comments - Add QueryCodeViewer component with custom drasi-light/drasi-dark themes matching the app's color palette - Strip leading blank lines and normalize indentation for clean display - Sync editor theme via MutationObserver on the html dark class Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reduce unnecessary re-renders with React.memo and useMemo - Wrap SourceNode, QueryNode, ReactionNode in React.memo() to skip re-renders when node data hasn't changed - Memoize pipelineData in App.tsx so FlowCanvas and all child nodes only re-render when sources/queries/reactions actually change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Optimize canvas re-renders, query streaming, and node internals - Add CanvasLockedContext so NodeShell reads canvas lock state from context instead of per-node data injection, preserving data references for React.memo to work correctly - Remove canvasLocked from node data spreading in FlowCanvas; only update draggable prop when lock state changes - Replace O(n) findIndex+stableKey lookups in useQueryResults with a Map<string, number> index for O(1) streaming event processing - Extract formatValue to module scope in QueryNode and memoize columns and displayRows with useMemo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Memoize inspector props, reduce animation overhead, fix MATCH highlighting - Memoize inspector props in App.tsx with useMemo to avoid rebuilding on every render - Replace Framer Motion motion.div/motion.button in NodeShell with plain elements using CSS transitions, eliminating JS-driven animation overhead - Memoize toolbar buttons JSX in NodeShell with useMemo - Replace animate-ping with animate-pulse on StatusBadge for lighter opacity-only animation instead of scale+fade - Fix Cypher tokenizer: prioritize keywords over function-call rule so MATCH highlights correctly when followed by parentheses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update for drasi-lib 0.4.x API changes and add UI build support Adapt drasi-server to breaking changes in drasi-lib 0.4.x: API compatibility fixes: - Remove Added/Removed variants from ComponentStatusDto (removed upstream) - Add BootstrapProvider/IdentityProvider to ComponentTypeDto (added upstream) - Update api_contract_test to match new ComponentStatus variants Dependency updates: - Bump drasi-lib from 0.3.8 to 0.4.0 - Bump all source plugins (mock, http, grpc, postgres, platform) - Bump all bootstrap plugins (postgres, scriptfile, platform, noop, application) - Bump all reaction plugins (log, http, grpc, sse, platform, profiler, etc.) - Bump index plugins (rocksdb 0.2.2 → 0.3.0, garnet 0.1.4 → 0.1.6) - Bump state store plugin (redb 0.1.5 → 0.1.6) - Update Cargo.lock accordingly UI build integration: - Add Node.js UI builder stage to Dockerfile (multi-stage) - Add build-ui, clean-ui targets to Makefile - Make build/run targets depend on build-ui - Copy built UI assets into Docker runtime image Test improvements: - Add integration test scaffolding for multi-instance API - Improve solution_helpers test support utilities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add local plugin build workflow and fix trading demo segfault The trading demo (and any config using autoInstallPlugins) crashed with SIGSEGV when the server was compiled against local drasi-core via [patch.crates-io]. Root cause: Cargo patches only affect compile-time dependency resolution for the server binary, but cdylib plugins are separate shared libraries loaded at runtime. Registry-downloaded plugins were compiled against published drasi-core and had a different #[repr(C)] vtable layout, causing an ABI mismatch and heap corruption. Changes: - Add 'make build-local-plugins' and 'make build-local-plugins-debug' Makefile targets that build all cdylib plugins from ../drasi-core and copy them to target/{release,debug}/plugins/ - Remove autoInstallPlugins and plugins list from trading config so it uses locally-built plugins instead of downloading from the registry - Update start-demo.sh to auto-build local plugins when none are found - Document the [patch.crates-io] vs runtime plugin distinction in CLAUDE.md with build commands Also includes: snapshot-based persistence refactor, clone instance endpoint, solution template roundtrip tests, and UI updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: full plugin-aware config validation for validate command Enhance 'drasi-server validate' to perform comprehensive config verification beyond YAML structure checking: - Load plugins from the plugins directory and check that all required plugin kinds (sources, reactions, bootstrappers) are available - Validate source/reaction/bootstrap configs against plugin OpenAPI schemas using the jsonschema crate (no FFI changes required) - Walk env var references in config JSON and report missing vars that have no defaults (both ${VAR} and ConfigValue patterns) - Skip ${{...}} Handlebars template expressions (not env var refs) - Report structured errors with component type, id, and field path - Gracefully degrade when plugins aren't installed (warnings, not failures) Also adds upfront plugin availability check at server startup that reports ALL missing plugins at once instead of failing one at a time. New files: - src/config/plugin_validation.rs (1311 lines, 27 unit tests) - tests/validate_cli_example_configs_test.rs (12 integration tests including 10 negative tests for invalid configs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): correct canvas node displacement on expand/collapse Replace anchor-point displacement logic with full bounding-box rules in both useAutoLayout.ts and NodeShell.tsx handleToggle. Old behavior used only C's top-left corner and N's top edge: if (a >= prevX2) shift right — correct but imprecise if (b >= N.y) shift down — WRONG: used N's top, not bottom New behavior uses full bounding boxes of both N and C: C fully above N (b2 <= y1) -> stationary C fully left of N (a2 <= x1) -> stationary C fully right of N (a1 >= x2) -> shift right by dx C below N bottom + horizontal overlap -> shift down by dy This fixes nodes being pushed down incorrectly when they were beside the expanding node at the same height, and fixes expanding nodes overlapping nodes directly below them because dy was applied based on N's top edge rather than its bottom edge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Working on UI Canvas Node expansion. Signed-off-by: Agent of Reality <agentx@agentofreality.com> * refactor(ui): use fixed target heights for node layout displacement Replace dynamic height measurement/lock/timer system with a static TARGET_DIMS lookup table for both width and height, matching how width already worked. This eliminates the prediction error that caused incorrect neighbor displacement when nodes expand. Changes: - useAutoLayout: Replace getEffectiveHeight, heightTargets, lockUntil, recalcTick, and pendingRecalcTimers with a simple getTargetHeight() from a TARGET_DIMS table. Remove nodeLookup/store dependency. Fix query expandedW (was 360, now correctly 420). - NodeShell: Add collapsedHeight/expandedHeight props with explicit CSS height transition. Remove DOM measurement in handleToggle, expandContentRef, useUpdateNodeInternals, and setTimeout timer. - Node components: Replace collapsedMinHeight with collapsedHeight and expandedHeight props matching TARGET_DIMS values. - index.css: Set edges z-index to 0 (below nodes instead of above). * fix: replace unwrap() with expect() in validate CLI test The clippy::unwrap_used deny rule flagged config_path.to_str().unwrap() in validate_cli_example_configs_test.rs. Replace with .expect() to provide a descriptive panic message if the path is not valid UTF-8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add commit-staged skill for project Add a reusable Copilot CLI skill at .github/skills/commit-staged/SKILL.md that provides instructions for committing staged files with accurate, conventional commit messages. This skill is project-scoped so all contributors can use it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: implement runtime plugin management system Implement the full runtime plugin management design from DESIGN-runtime-plugin-management.md across drasi-server. Host-sdk shared types (in drasi-core, separate commit): - PluginRegistry moved to host-sdk with RwLock, RegisteredDescriptor, deregistration methods, and version counter - PluginLockfile moved to host-sdk for reuse by all hosts - PluginLifecycleManager for load/retire with event broadcasting - PluginWatcher with notify crate for filesystem hot-reload - Plugin naming/discovery helpers and metadata-only scanning - plugin_id tracking on all proxy types - Display metadata (display_name/description/icon) on descriptor traits Server-side implementation: - PluginOperations shared service replacing duplicated CLI/startup helpers - PluginOrchestrator with drain-then-retire upgrade protocol - Plugin REST API at /api/v1/plugins/ (list, get, load, install, upgrade, promote, retire, dependents, kinds, schema, SSE events) - Component metadata stamping (pluginId/pluginVersion/pluginGeneration) - OpenAPI cache with registry version invalidation - Hot-reload config (hotReloadPlugins, hotReloadDebounceMs, hotReloadMode) - Watcher startup wiring when hot-reload is enabled - Init wizard: dynamic plugin discovery + registry download flow - UI: PluginManagementPanel, SchemaForm, usePluginKinds/usePluginSchema hooks, dynamic TypeSelector with API-driven fallback - CLI commands refactored to use PluginOperations shared helpers - Path traversal protection on POST /plugins/load - CLAUDE.md updated with new architecture and API endpoints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: local dir plugin source + init wizard select-or-install flow Add support for pluginRegistry being a local filesystem path alongside OCI URLs. When a path is detected (absolute, relative, drive letter, UNC, file:// URI), plugins are scanned/copied from that directory instead of downloaded from an OCI registry. Rewrite init wizard flow: remove upfront "download plugins?" prompt. Instead, each component selection step (sources, bootstrappers, reactions) shows locally installed plugins with an inline "Install from a registry" option. Registry can be an OCI URL or local path. After downloading, the list refreshes and the user selects again. Changes: - PluginOperations: search_registry() and install_from_registry() branch on PluginSourceKind internally — all callers unchanged - CLI plugin search/install/upgrade: support local dir sources - Startup auto-install: support local dir sources - Init wizard: select-or-install pattern per component category - attach_bootstrap_to_source for per-source bootstrap selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: improve init wizard prompts and settings - Add plugin settings prompts (pluginRegistry, verifyPlugins, autoInstallPlugins) to server settings section - Reorder prompts: plugin registry before hot-reload - Reorder DrasiServerConfig fields so plugin/hot-reload settings serialize before sources/queries/reactions in YAML output - Pass user's pluginRegistry as default for download prompts - Replace all with_help_message() calls with hint() helper that prints dim-colored descriptions on a separate line above each prompt, giving consistent look/feel across server settings, source, bootstrap, and reaction configuration sections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add left panel sidebar UI with 6 tabbed sections Replace the scattered right inspector, plugin overlay, event overlay, and top bar buttons with a unified left panel sidebar. Layout: fixed-width icon rail (always visible) + sliding content panel (320px, animated with framer-motion width transition). 6 sections accessible via icon rail: - Components: searchable catalog with filter chips, click to create - Solutions: template list with deploy/delete/create/upload actions - Plugins: plugin list with search, type filters, expand/collapse - Instances: instance cards with switch/clone/create actions - Logs: event log with search and type filter chips - Selected Component: source/query/reaction inspector (bottom icon, active only when a canvas node is selected) Top bar simplified to: logo, instance selector, connection dot, theme. Inspector content redesigned for narrow width: stacked config layout, compact connected component rows, overflow handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: auto-hide left panel on canvas click when not pinned When the sidebar is not pinned, clicking the canvas background now closes the panel in addition to deselecting components. Pinned panels remain open on canvas clicks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: replace health polling with SSE connection state Remove the /health endpoint polling (every 5s) and replace it with reactive connection state tracking from the existing SSE EventSource. The UI already maintains a persistent SSE connection for component events. The EventSource onopen/onerror/onmessage callbacks now drive the connection indicator — no polling needed. Three states instead of two: - "Live" (green pulsing) — SSE connection open - "Connecting..." (amber pulsing) — SSE reconnecting - "Disconnected" (red) — SSE connection closed New hook: useConnectionState(instanceId) wraps the SSE state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: derive component catalog from installed plugins only Remove hardcoded builtin source/reaction lists from ComponentsPanel. The catalog now shows only components whose plugins are actually installed, derived from GET /api/v1/plugins/kinds. Add two query types: openCypher Query and GQL Query (queries are built into drasi-lib and always available regardless of plugins). Known kinds retain friendly labels, icons, and descriptions when their plugin is installed. Unknown plugin kinds show a generic Puzzle icon with "Plugin-provided source/reaction" description. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: schema-driven config editor + OpenAPI plugin annotations - YAML-only config editor with monaco-yaml schema validation, autocomplete, and pre-populated templates from plugin schemas - Schema resolver: resolves $ref, adds oneOf titles from discriminator, generates YAML skeleton with comments - Remove all hardcoded source/reaction forms — all plugin config driven by schema from GET /plugins/kinds/{cat}/{kind}/schema - Component inspector shows config as read-only YAML viewer - Fix plugin route nesting so /kinds/{cat}/{kind}/schema works - Add #[utoipa::path] annotations to all 11 plugin endpoints - Add OpenAPI tests for plugin paths and schemas Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * rename build-test-plugins to build-local-test-plugins Rename the Makefile target to match the naming convention of build-local-plugins and build-local-plugins-debug. The new target now copies built test plugins into target/debug/plugins/ (instead of leaving them in ../drasi-core/target/debug/), and tests load plugins from that local path via CARGO_MANIFEST_DIR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: improve UI error visibility, SSE event handling, and Monaco setup Server DTO layer: - Add Added/Removed variants to ComponentStatusDto so the UI receives proper structural events when components are added or deleted - Detect add/remove from drasi-core's message convention in ComponentEventDto::from() conversion UI error handling: - Add axios response interceptor to reject {success:false} responses as errors (server returns HTTP 200 with error body for many failures) - Wrap all inspector start/stop/delete handlers with async/try/catch so API errors are caught and displayed in the Logs panel - Merge SSE component events and user-action events into a single stream for the Logs panel (previously pushEvent messages were invisible) UI SSE event log: - Add useComponentEventLog hook that subscribes to the shared SSE stream and converts ComponentGraph events into log entries - Export subscribeComponentEvents from useApi for external subscribers Monaco Editor: - Downgrade monaco-editor from 0.55.1 to 0.52.2 to fix incompatibility with monaco-yaml/monaco-worker-manager (0.55 changed createWebWorker API from moduleId-based to Worker-based, breaking YAML language services) - Add monaco-setup.ts with local worker configuration via getWorker - Configure @monaco-editor/react loader to use local monaco instance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): sort sidebar panel items alphabetically Sort items in all four left-panel lists by their display name: - Components panel: sorted by label - Solutions panel: sorted by name - Plugins panel: sorted by id - Instances panel: sorted by id Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): set queryLanguage based on selected query type Pass the query kind ("cypher" or "gql") from the Components panel through to startDraft instead of hardcoding "query". The draft defaults now set queryLanguage to "GQL" when GQL Query is selected and "Cypher" when openCypher Query is selected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(plugins): add registry search API and install dialog Add GET /api/v1/plugins/registry/search endpoint that queries the configured (or overridden) plugin registry and returns available plugins matching a search query. Add InstallPluginDialog UI component that lets users browse a plugin registry, select plugins, and batch-install them with per-plugin progress tracking. Integrate the dialog into PluginsPanel via a download button in the toolbar. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add hot-reload plugin tests and test-all Makefile target Add comprehensive tests for the hotReloadPlugins feature: - New tests/hot_reload_test.rs with 11 tests covering: - PluginOrchestrator.load_plugin() with real cdylib plugins - Hot-loaded plugin kinds available in registry - Multiple plugin loading, reload-same-plugin behavior - PluginWatcher → PluginOrchestrator event-driven pipeline - Full DrasiServer E2E: config with hotReloadPlugins: true, drop plugin at runtime, verify via REST API, create component - Side-by-side mode skip behavior - Watcher filtering, removal detection, change detection Add `make test-all` target that runs every available test: - Builds cdylib test plugins (including http reaction) - Runs all cargo tests with --include-ignored - Runs doctests, plugin smoke tests, and VSCode tests Also fix pre-existing issues uncovered by test-all: - Add drasi-reaction-http to build-local-test-plugins (needed by solution_e2e_data_flow_test.rs) - Fix misleading #[ignore] messages to name actual plugin deps and point to `make build-local-test-plugins` Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: standardize plugin API error responses on ErrorResponse Plugin endpoints used ad-hoc serde_json::json!({"error": ..., "message": ...}) while component endpoints used the structured ErrorResponse type from shared/error.rs. This forced API consumers to handle different error shapes depending on the endpoint. Changes: - Add 12 plugin-specific error codes to error_codes module (PLUGIN_NOT_FOUND, PLUGIN_LOAD_FAILED, etc.) - Update status_from_code() to map plugin codes to HTTP statuses - Add ErrorResponse::into_json_response() for handlers returning (StatusCode, Json<Value>) tuples - Convert all 18 plugin handler error sites to use ErrorResponse with typed error codes - Add tests for new codes, status mappings, and into_json_response Plugin errors now return {"code": "PLUGIN_...", "message": "..."} matching the same envelope as component ErrorResponse. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: replace default instance wrappers with middleware Remove 25+ *_default handler wrapper functions (~480 lines) from v1/handlers.rs that each extracted the default instance and forwarded to shared handlers. Replace with a single resolve_default_instance middleware in routes.rs that injects Extension<Arc<DrasiLib>> and Extension<String> (instance_id) into the request, allowing the shared handlers to serve convenience routes directly. Net reduction: -467 lines of near-identical boilerplate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: validate path id matches body id in upsert handlers PUT /api/v1/.../sources/{id} and PUT /api/v1/.../reactions/{id} previously ignored the path {id} parameter and silently used the body's id field, allowing mismatches to go undetected. Both upsert_source_handler and upsert_reaction_handler now accept Path(path_id) and return an error if it doesn't match the body id. The v1 handler wrappers forward the path id from ResourcePath. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add path traversal protection to upgrade_plugin endpoint The load_plugin handler correctly canonicalized paths and verified containment within the plugins directory, but upgrade_plugin joined the user-supplied filename directly without any traversal check. An attacker could supply "../../etc/malicious.so" to load arbitrary shared libraries from outside the plugins directory. Apply the same canonicalize + starts_with containment check used by load_plugin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf: compile variable regexes once with LazyLock The ${VAR} and ${VAR:-default} regex patterns in extract_variables() and resolve_yaml_variables() were recompiled on every function call using Regex::new().expect(). Move them to static LazyLock<Regex> so they compile once on first use and are reused thereafter. Add test_var_extract_regex_compiles and test_var_resolve_regex_compiles to verify the static patterns are valid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace expect() with error handling in get_query handler serde_json::to_value().expect() in get_query would panic the request handler if QueryConfigDto serialization failed (e.g., non-finite floats). Return 500 with a log message instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve server-level settings across config persistence saves ConfigPersistence::save() hard-coded defaults for plugin_registry, auto_install_plugins, plugins, verify_plugins, trusted_identities, hot_reload_plugins, hot_reload_debounce_ms, hot_reload_mode, and enable_ui. The first save after startup would erase these settings from the config file. Store original server-level settings in a PreservedServerSettings struct at construction time and use them when reconstructing the DrasiServerConfig during save. Add test_save_preserves_server_level_settings to verify all 9 settings su…
1 parent 38670ba commit 541bff1

235 files changed

Lines changed: 49861 additions & 9256 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
name: commit-staged
3+
description: Commit all staged files with an accurate, informative commit message. Use when asked to commit, make a commit, or save changes.
4+
---
5+
6+
1. Run `git diff --cached` to review all staged changes.
7+
2. Analyze the changes to understand what was modified and why.
8+
3. Write a commit message following conventional commit style:
9+
- A concise subject line (≤72 chars) summarizing the change
10+
- A blank line followed by a body explaining what and why
11+
- Include the Co-authored-by trailer
12+
4. Let the user review the commit message and make edits if necessary before finalizing the commit. Never commit without user approval of the message.
13+
5. Commit with `git commit -m "..."`.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ logs/
3737

3838
# Vendored native libraries (downloaded from OCI registry)
3939
vendor/
40+
41+
# UI build artifacts
42+
ui/node_modules/
43+
ui/dist/
44+
45+
.vscode

.vscode/extensions.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

.vscode/launch.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

.vscode/mcp.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

.vscode/settings.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

.vscode/tasks.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

CLAUDE.md

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,28 @@ This is the Drasi Server repository - a standalone server wrapper around DrasiLi
1414
- Cross-compile: `make build-cross TARGET=x86_64-pc-windows-gnu`
1515
- Run server: `cargo run` or `cargo run -- --config config/server.yaml`
1616
- Run with custom port: `cargo run -- --port 8080`
17-
- Run with plugin verification: `cargo run -- --verify-plugins --config config/server.yaml`
17+
- Run with plugin verification disabled: `cargo run -- --skip-verification --config config/server.yaml`
18+
- Run with UI disabled: `cargo run -- --disable-ui`
19+
- Run with UI enabled (override config): `cargo run -- --enable-ui`
20+
- Validate config (structure only): `cargo run -- validate --config config/server.yaml`
21+
- Validate config (with plugins): `cargo run -- validate --config config/server.yaml --plugins-dir ./plugins`
1822
- Check compilation: `cargo check`
1923

2024
### Plugin Loading
2125
Plugins (sources, reactions, bootstrap providers) are loaded at runtime as cdylib shared libraries (`.so`/`.dylib`/`.dll`) from a `plugins/` directory next to the binary. Each plugin is self-contained with its own tokio runtime, communicating via a stable C ABI. Plugin building is managed by drasi-core, not this repository.
2226

27+
**Important: `[patch.crates-io]` does NOT affect plugins.** Cargo patches only affect compile-time dependency resolution for the server binary. Plugins are separate shared libraries loaded at runtime — they must be built separately. When developing with local drasi-core changes, always use `make build-local-plugins` to rebuild plugins from local source. Registry-downloaded plugins (`autoInstallPlugins: true`) will NOT be ABI-compatible with local drasi-core changes.
28+
29+
- Build all plugins from local drasi-core (release): `make build-local-plugins`
30+
- Build all plugins from local drasi-core (debug): `make build-local-plugins-debug`
31+
- Build test-only plugins (mock, log, scriptfile): `make build-local-test-plugins`
32+
- Download test plugins from OCI registry (no drasi-core needed): `make download-test-plugins`
33+
34+
**Local directory plugin sources:** The `pluginRegistry` config field (and `--registry` CLI flag) accepts local filesystem paths in addition to OCI registry URLs. When a path is detected (e.g., `/path/to/plugins`, `./plugins`, `../drasi-core/target/debug/plugins`, `file:///opt/plugins`), the system scans the directory for plugin binaries instead of contacting an OCI registry. This is useful for development workflows where plugins are built locally. Detection is cross-platform: Unix absolute paths, relative paths (`./`, `../`), home-relative (`~/`), `file://` URIs, Windows drive letters, and UNC paths are all recognized as local directories.
35+
2336
### Testing
2437
- Run all tests: `cargo test`
38+
- Run all tests including plugin-dependent: `make test-all`
2539
- Run unit tests only: `cargo test --lib`
2640
- Run specific test: `cargo test test_name`
2741
- Run integration tests: `./tests/run_working_tests.sh`
@@ -43,13 +57,17 @@ This repository contains only the server wrapper functionality:
4357
1. **Server** (`src/server.rs`) - Main server implementation that wraps DrasiLib
4458
2. **API** (`src/api/`) - REST API implementation with OpenAPI documentation
4559
- `v1/` - API version 1 handlers, routes, and OpenAPI spec
60+
- `v1/plugin_handlers.rs` - Plugin management API endpoints
4661
- `shared/` - Common handlers, error types, and response types shared across versions
4762
- `version.rs` - API version constants and utilities
4863
- `models/` - Data Transfer Objects (DTOs)
4964
- `mappings/` - DTO to domain model conversions
5065
3. **Builder** (`src/builder.rs`) - Builder pattern for server construction
5166
4. **Main** (`src/main.rs`) - CLI entry point for standalone server
5267
5. **Dynamic Loading** (`src/dynamic_loading.rs`) - Runtime plugin loading from shared libraries
68+
6. **Plugin Operations** (`src/plugin_operations.rs`) - Shared plugin management service used by CLI, init, startup, and API
69+
7. **Plugin Orchestrator** (`src/plugin_orchestrator.rs`) - Server-level plugin lifecycle coordination (load, install, track)
70+
8. **Plugin Registry** (`src/plugin_registry.rs`) - Re-exports from host-sdk: mutable registry with `Arc<RwLock<PluginRegistry>>`
5371

5472
### Core Components (External Dependency)
5573

@@ -102,7 +120,12 @@ port: 8080
102120
logLevel: "info"
103121
persistConfig: true # Enable persistence (default)
104122
persistIndex: false # Use RocksDB for persistent indexing (default: false, uses in-memory)
105-
verifyPlugins: true # Enable cosign signature verification for downloaded plugins (default: false)
123+
verifyPlugins: true # Enable cosign signature verification for downloaded plugins (default: true)
124+
enableUi: true # Enable the web UI at /ui (default: true)
125+
126+
# Hot-reload plugin settings (default: all disabled)
127+
# hotReloadPlugins: false # Enable filesystem watching for plugin changes
128+
# hotReloadDebounceMs: 2000 # Debounce window in milliseconds
106129

107130
# Optional trusted identities for plugin signature verification
108131
# trustedIdentities:
@@ -121,6 +144,12 @@ verifyPlugins: true # Enable cosign signature verification for downloaded plugi
121144
# defaultDispatchBufferCapacity: 1000
122145
# defaultDispatchBufferCapacity: "${DISPATCH_BUFFER_CAPACITY:-1000}"
123146

147+
# CORS allowed origins (default: empty = all origins permitted)
148+
# When set, only listed origins are allowed for cross-origin requests.
149+
# corsAllowedOrigins:
150+
# - "http://localhost:3000"
151+
# - "https://dashboard.example.com"
152+
124153
# Sources (parsed into plugin instances)
125154
sources:
126155
- kind: mock
@@ -182,6 +211,8 @@ The REST API is exposed under `/api/v1/instances/{instanceId}/...` for multi-ins
182211

183212
### Configuration Persistence
184213

214+
Persistence uses a snapshot-based approach: when saving, `ConfigPersistence::save()` calls `snapshot_configuration()` on each DrasiLib instance via the ComponentGraph. The ComponentGraph is the single source of truth — there are no shadow caches or separate registration steps. Mutations flow through the ComponentGraph, and the persisted YAML is reconstructed from the current graph state at save time.
215+
185216
DrasiServer separates two independent concepts:
186217

187218
1. **Persistence** - Whether API changes are saved to the config file
@@ -206,7 +237,7 @@ DrasiServer separates two independent concepts:
206237
- This allows dynamic query creation without persistence (useful for programmatic usage)
207238

208239
**Behavior:**
209-
- When persistence enabled: all API mutations (create/delete queries) are automatically saved to the config file using atomic writes (temp file + rename) to prevent corruption
240+
- When persistence enabled: `save()` snapshots component state from the ComponentGraph and writes to YAML using atomic writes (temp file + rename) to prevent corruption
210241
- When persistence disabled: API mutations work but changes are lost on restart
211242
- When read-only: all create/delete operations via API are rejected
212243

@@ -291,6 +322,8 @@ The server exposes a versioned REST API on port 8080 by default. All API endpoin
291322
### Instance Management
292323

293324
- `GET /api/v1/instances` - List all DrasiLib instances
325+
- `GET /api/v1/instances/{instanceId}/snapshot` - Get configuration snapshot of an instance
326+
- `POST /api/v1/instances/{instanceId}/clone` - Clone components from another instance
294327

295328
### Component Management (Instance-Specific)
296329

@@ -328,12 +361,74 @@ For convenience, the first configured instance is accessible via shortened route
328361
- `GET/POST /api/v1/queries` - Queries of the first instance
329362
- `GET/POST /api/v1/reactions` - Reactions of the first instance
330363

364+
### Plugin Management (Server-Wide)
365+
366+
Plugin endpoints are server-wide (not per-instance) since plugins are shared across all instances:
367+
- `GET /api/v1/plugins` - List all loaded plugins with status
368+
- `GET /api/v1/plugins/{pluginId}` - Get plugin details
369+
- `POST /api/v1/plugins/load` - Load a plugin from disk
370+
- `POST /api/v1/plugins/install` - Install from remote OCI registry
371+
- `GET /api/v1/plugins/{pluginId}/dependents` - List dependent components
372+
- `GET /api/v1/plugins/kinds` - List all available kinds (sources, reactions, bootstrappers)
373+
- `GET /api/v1/plugins/kinds/{category}/{kind}/schema` - Get config schema for a kind
374+
331375
## Important Patterns
332376

333377
### Error Handling
334-
- Use `anyhow::Result` for functions that can fail
335-
- Custom `DrasiError` type for domain-specific errors
336-
- Proper error propagation with `?` operator
378+
379+
Drasi Server uses a three-layer error pattern aligned with drasi-lib:
380+
381+
**Layer 1 — HTTP handlers → `ErrorResponse`:**
382+
All API error responses use `ErrorResponse` from `src/api/shared/error.rs`, which implements
383+
`IntoResponse` to automatically set the HTTP status code and serialize a structured
384+
`{code, message, details?}` JSON body. Handlers return `Result<Json<ApiResponse<T>>, ErrorResponse>`.
385+
386+
```rust
387+
// Good: handler returns ErrorResponse on failure
388+
Err(ErrorResponse::new(error_codes::SOURCE_NOT_FOUND, "Source 'x' not found"))
389+
390+
// Good: convert DrasiError to ErrorResponse automatically
391+
Err(ErrorResponse::from(drasi_error))
392+
393+
// Bad: DO NOT return bare StatusCode without body
394+
Err(StatusCode::NOT_FOUND) // ← No error body!
395+
396+
// Bad: DO NOT return 200 OK with error in body
397+
Ok(Json(ApiResponse::error("something failed"))) // ← Wrong status code!
398+
```
399+
400+
**Layer 2 — Server services → `anyhow::Result`:**
401+
Internal modules (server.rs, persistence.rs, factories.rs, plugin_orchestrator.rs, config/)
402+
use `anyhow::Result` with `.context()` for rich error chains. These convert to `ErrorResponse`
403+
at the handler boundary via `From<anyhow::Error>`.
404+
405+
**Layer 3 — drasi-lib → `DrasiError`:**
406+
Calls to `DrasiLib` return `DrasiError` which converts to `ErrorResponse` via `From<DrasiError>`
407+
with proper status code mapping (ComponentNotFound → 404, AlreadyExists → 409, etc.).
408+
409+
**Error codes** are defined in `src/api/shared/error.rs::error_codes` module. Use existing codes
410+
or add new ones — never use ad-hoc string error codes.
411+
412+
**Rules for contributors:**
413+
- Never return bare `Err(StatusCode::...)` from handlers — always use `ErrorResponse`
414+
- Never return `Ok(Json(ApiResponse::error(...)))` — errors must have proper HTTP status codes
415+
- Use `ErrorResponse::from(drasi_error)` to convert DrasiLib errors
416+
- Use `anyhow::Result` with `.context()` in internal/service modules
417+
- Add new error codes to `error_codes` module when needed
418+
- **Never embed raw underlying error strings in `ErrorResponse.message`.** The
419+
`message` field is a high-level human-readable description of the failure.
420+
Underlying technical errors (`e.to_string()`, file paths, `DrasiError`
421+
internals) belong in `ErrorDetail::technical_details`.
422+
423+
**Persistence failures:** Mutating handlers (create/delete source/query/
424+
reaction, instance create, clone, etc.) call `persist_after_operation` which
425+
returns `Result<(), ErrorResponse>`. If persistence fails after the in-memory
426+
mutation has already succeeded, the helper returns `PERSISTENCE_FAILED` (HTTP
427+
500) — handlers `?`-propagate this. The error message states explicitly that
428+
the change was applied in memory but not persisted; the underlying
429+
filesystem error (`e.to_string()`) is in `ErrorDetail::technical_details`.
430+
Operators must retry the operation or restart after fixing the underlying
431+
issue, since the runtime is now ahead of the on-disk YAML.
337432

338433
### Async/Await
339434
- All I/O operations are async using Tokio
@@ -342,6 +437,8 @@ For convenience, the first configured instance is accessible via shortened route
342437

343438
### State Management
344439
- Components track their status (Stopped/Starting/Running/Stopping/Failed)
440+
- Plugins track their lifecycle (Loaded/Active/Failed)
441+
- Plugin registry uses `Arc<RwLock<PluginRegistry>>` for concurrent read/write access
345442
- Configuration persisted to YAML files (when persistence enabled)
346443
- In-memory state for active components
347444

@@ -429,4 +526,8 @@ server.run().await?;
429526
- Plugin metadata validation checks SDK version (major.minor match) and target triple at load time
430527
- All data processing logic resides in drasi-lib
431528
- This repository focuses on API and server lifecycle management
432-
- Plugin signature verification is available via `--verify-plugins` CLI flag or `verifyPlugins: true` in config. Uses Sigstore/cosign keyless verification against the Rekor transparency log.
529+
- Plugin signature verification is enabled by default (`verifyPlugins: true` in config). Use `--skip-verification` CLI flag or `verifyPlugins: false` to disable. Uses Sigstore/cosign keyless verification against the Rekor transparency log.
530+
- **Plugin lifecycle management**: Plugins can be loaded and installed at runtime via the `/api/v1/plugins/` API. Dynamic upgrade/replacement of a running plugin is not currently supported — restart the server to replace a plugin.
531+
- **Plugin registry is mutable**: Uses `Arc<RwLock<PluginRegistry>>` — shared types (PluginRegistry, PluginLockfile, PluginLifecycleManager, PluginWatcher) live in `drasi-host-sdk`, re-exported by this repo
532+
- **Component metadata**: Sources and reactions carry `pluginId` and `pluginVersion` in their ComponentGraph metadata
533+
- **Shared plugin operations**: `PluginOperations` in `src/plugin_operations.rs` provides the single source of truth for plugin management used by CLI, init, startup, and API

0 commit comments

Comments
 (0)