Files
attune/docs/architecture/web-ui-architecture.md
2026-02-04 17:46:30 -06:00

36 KiB

Web UI Architecture

Created: 2024-01-18
Status: Planning
Tech Stack: React 18 + TypeScript + Vite

Overview

The Attune Web UI is a single-page application (SPA) that provides a comprehensive interface for managing and monitoring the Attune automation platform. It communicates with the Attune API service via REST endpoints and receives real-time updates through WebSocket connections to the notifier service.

Architecture Principles

  1. Type Safety: Full TypeScript coverage with types generated from OpenAPI specification
  2. Real-time Updates: WebSocket integration for live execution monitoring and notifications
  3. Offline-first Caching: Intelligent client-side caching with optimistic updates
  4. Component Reusability: Shared components for common patterns (lists, forms, detail views)
  5. Performance: Code splitting, lazy loading, and efficient re-rendering
  6. Developer Experience: Fast dev server, hot module replacement, clear error messages

Technology Stack

Core Framework

React 18

  • Purpose: UI component library and rendering engine
  • Why: Industry standard, mature ecosystem, excellent for complex UIs
  • Key Features Used:
    • Hooks for state management (useState, useEffect, useContext)
    • Suspense for code splitting and async data loading
    • Concurrent rendering for improved UX
    • Error boundaries for graceful error handling

TypeScript 5.x

  • Purpose: Type safety and improved developer experience
  • Why: Catches errors at compile time, better IDE support, self-documenting code
  • Configuration:
    • Strict mode enabled
    • Path aliases for clean imports (@/components, @/api, @/hooks)
    • Integration with OpenAPI-generated types

Vite

  • Purpose: Build tool and dev server
  • Why: Fast HMR, optimized production builds, native ESM support
  • Features:
    • Sub-second dev server startup
    • Instant hot module replacement
    • Optimized production builds with Rollup
    • Environment variable management
    • Plugin ecosystem (React, TypeScript support out of the box)

API Layer

OpenAPI TypeScript Codegen

  • Purpose: Generate type-safe API client from OpenAPI spec
  • Why: Single source of truth, automatic updates when API changes
  • Generated Artifacts:
    • TypeScript interfaces for all DTOs (requests/responses)
    • API client with methods for all 86 endpoints
    • Type-safe parameter validation
    • Automatic bearer token injection

Usage Pattern:

// Generated client usage
import { ActionsService } from '@/api/services/ActionsService';

// Type-safe API call with auto-complete
const actions = await ActionsService.listActions({
  limit: 50,
  offset: 0
});
// actions is typed as ApiResponse<PaginatedResponse<ActionSummary[]>>

Code Generation Command:

# Run whenever API spec changes
npm run generate:api

# Implemented as:
openapi-typescript-codegen \
  --input http://localhost:8080/api-spec/openapi.json \
  --output ./src/api \
  --client axios \
  --useOptions

Configuration (openapi-codegen.config.json):

  • Client: axios (configurable for auth, interceptors)
  • Type generation: all request/response types
  • Service generation: one service class per tag (PacksService, ActionsService, etc.)
  • Enum handling: TypeScript enums for all OpenAPI enums

Axios

  • Purpose: HTTP client for API requests
  • Why: Interceptors for auth, request/response transformation, browser/node compatibility
  • Configuration:
    • Base URL from environment variable
    • Request interceptor: inject JWT token from storage
    • Response interceptor: handle 401 (refresh token), network errors
    • Timeout configuration
    • Request/response logging in development

Axios Instance Setup:

// src/api/client.ts
import axios from 'axios';
import { getAccessToken, refreshAccessToken } from '@/auth/tokens';

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor: inject auth token
apiClient.interceptors.request.use((config) => {
  const token = getAccessToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor: handle auth errors
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // If 401 and not already retried, attempt token refresh
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        await refreshAccessToken();
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh failed, redirect to login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

Data Fetching & Caching

TanStack Query (React Query v5)

  • Purpose: Server state management, caching, and synchronization
  • Why: Eliminates boilerplate, automatic caching, background refetching, optimistic updates
  • Key Features:
    • Automatic background refetching
    • Cache invalidation and updates
    • Optimistic updates for better UX
    • Request deduplication
    • Pagination and infinite scroll support
    • Prefetching for improved perceived performance

Query Usage Patterns:

// src/hooks/useActions.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ActionsService } from '@/api/services/ActionsService';

// Query for list of actions
export function useActions(params?: { pack_ref?: string; limit?: number }) {
  return useQuery({
    queryKey: ['actions', params],
    queryFn: () => ActionsService.listActions(params),
    staleTime: 30000, // Consider fresh for 30s
    gcTime: 5 * 60 * 1000, // Keep in cache for 5 min
  });
}

// Query for single action
export function useAction(ref: string) {
  return useQuery({
    queryKey: ['actions', ref],
    queryFn: () => ActionsService.getActionByRef({ ref }),
    enabled: !!ref, // Only fetch if ref is provided
  });
}

// Mutation for creating action
export function useCreateAction() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: ActionsService.createAction,
    onSuccess: (newAction) => {
      // Invalidate action list to trigger refetch
      queryClient.invalidateQueries({ queryKey: ['actions'] });
      
      // Optimistically update cache with new action
      queryClient.setQueryData(['actions', newAction.ref], newAction);
    },
  });
}

// Mutation with optimistic update
export function useUpdateAction() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: ({ ref, data }: { ref: string; data: UpdateActionRequest }) =>
      ActionsService.updateAction({ ref, requestBody: data }),
    onMutate: async ({ ref, data }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['actions', ref] });
      
      // Snapshot previous value
      const previous = queryClient.getQueryData(['actions', ref]);
      
      // Optimistically update
      queryClient.setQueryData(['actions', ref], (old: any) => ({
        ...old,
        ...data,
      }));
      
      return { previous };
    },
    onError: (err, variables, context) => {
      // Rollback on error
      queryClient.setQueryData(['actions', variables.ref], context?.previous);
    },
    onSettled: (data, error, variables) => {
      // Refetch after mutation
      queryClient.invalidateQueries({ queryKey: ['actions', variables.ref] });
    },
  });
}

Query Key Strategy:

  • Use hierarchical keys: ['resource', params]
  • Examples:
    • ['actions'] - all actions
    • ['actions', { pack_ref: 'core' }] - actions filtered by pack
    • ['actions', 'core.http'] - specific action
    • ['executions', { status: 'running' }] - filtered executions

Cache Invalidation Patterns:

  • After mutations: invalidate related queries
  • WebSocket updates: update specific cache entries
  • Manual refresh: invalidate and refetch
  • Periodic background updates for critical data (running executions)

Authentication

JWT Token Management

  • Storage: Access token in memory, refresh token in httpOnly cookie (if backend supports) or localStorage
  • Flow:
    1. User logs in → receives access token (1h) + refresh token (7d)
    2. Store tokens securely
    3. Axios interceptor adds access token to all requests
    4. On 401 response, attempt refresh
    5. If refresh succeeds, retry original request
    6. If refresh fails, redirect to login

Auth Context:

// src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { AuthService } from '@/api/services/AuthService';

interface User {
  id: number;
  username: string;
  email: string;
  roles: string[];
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (username: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshUser: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  
  // Load user on mount
  useEffect(() => {
    loadUser();
  }, []);
  
  const loadUser = async () => {
    try {
      const response = await AuthService.getCurrentUser();
      setUser(response.data);
    } catch (error) {
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  };
  
  const login = async (username: string, password: string) => {
    const response = await AuthService.login({ requestBody: { username, password } });
    localStorage.setItem('access_token', response.data.access_token);
    localStorage.setItem('refresh_token', response.data.refresh_token);
    await loadUser();
  };
  
  const logout = async () => {
    localStorage.removeItem('access_token');
    localStorage.removeItem('refresh_token');
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{
      user,
      isAuthenticated: !!user,
      isLoading,
      login,
      logout,
      refreshUser: loadUser,
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

Protected Routes:

// src/components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAuth();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return <>{children}</>;
}

Real-time Updates

WebSocket Client

  • Purpose: Receive real-time notifications from notifier service
  • Connection: WebSocket to notifier service (separate from API)
  • Protocol: JSON messages with event types
  • Reconnection: Automatic reconnection with exponential backoff

WebSocket Integration:

// src/websocket/client.ts
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';

interface NotificationMessage {
  type: 'execution.started' | 'execution.completed' | 'execution.failed' | 
        'inquiry.created' | 'event.created';
  data: any;
  timestamp: string;
}

export function useWebSocketNotifications() {
  const wsRef = useRef<WebSocket | null>(null);
  const queryClient = useQueryClient();
  const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
  const reconnectAttempts = useRef(0);
  
  useEffect(() => {
    const connect = () => {
      const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8081';
      const token = localStorage.getItem('access_token');
      
      wsRef.current = new WebSocket(`${wsUrl}?token=${token}`);
      
      wsRef.current.onopen = () => {
        console.log('WebSocket connected');
        reconnectAttempts.current = 0;
      };
      
      wsRef.current.onmessage = (event) => {
        const message: NotificationMessage = JSON.parse(event.data);
        handleNotification(message);
      };
      
      wsRef.current.onclose = () => {
        console.log('WebSocket disconnected');
        scheduleReconnect();
      };
      
      wsRef.current.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
    };
    
    const scheduleReconnect = () => {
      const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
      reconnectAttempts.current++;
      
      reconnectTimeoutRef.current = setTimeout(() => {
        connect();
      }, delay);
    };
    
    const handleNotification = (message: NotificationMessage) => {
      switch (message.type) {
        case 'execution.started':
        case 'execution.completed':
        case 'execution.failed':
          // Update execution in cache
          queryClient.setQueryData(
            ['executions', message.data.id],
            message.data
          );
          // Invalidate execution lists
          queryClient.invalidateQueries({ queryKey: ['executions'] });
          break;
          
        case 'inquiry.created':
          // Add notification to UI
          queryClient.invalidateQueries({ queryKey: ['inquiries'] });
          break;
          
        case 'event.created':
          // Update event stream
          queryClient.invalidateQueries({ queryKey: ['events'] });
          break;
      }
    };
    
    connect();
    
    return () => {
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, [queryClient]);
}

Usage in App:

// src/App.tsx
function App() {
  useWebSocketNotifications(); // Connect once at app level
  
  return (
    <AuthProvider>
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    </AuthProvider>
  );
}

Workflow Visualization

React Flow

  • Purpose: Visual workflow editor and execution graph display
  • Why: Best-in-class workflow visualization library for React
  • Features:
    • Drag-and-drop node creation
    • Custom node types (action, decision, parallel, inquiry)
    • Edge routing and validation
    • Minimap and controls
    • Export to image
    • Zoom and pan

Workflow Editor Example:

// src/components/WorkflowEditor.tsx
import ReactFlow, { 
  Background, 
  Controls, 
  MiniMap,
  addEdge,
  useNodesState,
  useEdgesState,
} from 'reactflow';
import 'reactflow/dist/style.css';

const nodeTypes = {
  action: ActionNode,
  decision: DecisionNode,
  parallel: ParallelNode,
  inquiry: InquiryNode,
};

export function WorkflowEditor({ workflow }: { workflow: Workflow }) {
  const [nodes, setNodes, onNodesChange] = useNodesState(
    convertWorkflowToNodes(workflow)
  );
  const [edges, setEdges, onEdgesChange] = useEdgesState(
    convertWorkflowToEdges(workflow)
  );
  
  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
  );
  
  return (
    <div style={{ height: '600px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        nodeTypes={nodeTypes}
        fitView
      >
        <Background />
        <Controls />
        <MiniMap />
      </ReactFlow>
    </div>
  );
}

Custom Node Types:

  • ActionNode: Represents an action execution with status indicator
  • DecisionNode: Conditional branching with expression display
  • ParallelNode: Concurrent execution branches
  • InquiryNode: Human-in-the-loop interaction points

Code/YAML Editing

Monaco Editor

  • Purpose: Rich code/YAML editor for workflows, rules, and configurations
  • Why: Same editor as VS Code, excellent language support
  • Features:
    • Syntax highlighting for YAML, JSON, Python, JavaScript
    • Auto-completion
    • Error detection and linting
    • Diff editor for comparing versions
    • Themes (light/dark)

Monaco Integration:

// src/components/CodeEditor.tsx
import Editor from '@monaco-editor/react';

interface CodeEditorProps {
  value: string;
  onChange: (value: string) => void;
  language: 'yaml' | 'json' | 'python' | 'javascript';
  readOnly?: boolean;
}

export function CodeEditor({ 
  value, 
  onChange, 
  language, 
  readOnly = false 
}: CodeEditorProps) {
  return (
    <Editor
      height="400px"
      language={language}
      value={value}
      onChange={(val) => onChange(val || '')}
      theme="vs-dark"
      options={{
        readOnly,
        minimap: { enabled: false },
        lineNumbers: 'on',
        scrollBeyondLastLine: false,
        automaticLayout: true,
      }}
    />
  );
}

YAML Validation:

// src/utils/yamlValidation.ts
import yaml from 'js-yaml';
import Ajv from 'ajv';

export function validateWorkflowYAML(content: string): ValidationResult {
  try {
    const parsed = yaml.load(content);
    // Validate against workflow schema
    const ajv = new Ajv();
    const valid = ajv.validate(workflowSchema, parsed);
    
    if (!valid) {
      return { valid: false, errors: ajv.errors };
    }
    
    return { valid: true, data: parsed };
  } catch (error) {
    return { 
      valid: false, 
      errors: [{ message: error.message }] 
    };
  }
}

UI Components

shadcn/ui

  • Purpose: High-quality, accessible component library
  • Why: Copy-paste components (no NPM bloat), fully customizable, Tailwind-based
  • Components Used:
    • Button, Input, Select, Checkbox
    • Table, Card, Dialog, Popover
    • Tabs, Accordion, Sheet (side panel)
    • Toast (notifications)
    • Command (command palette)
    • Form (with react-hook-form integration)

Installation: Components are copied into project, not installed as dependency

Usage Pattern:

// src/components/actions/ActionList.tsx
import { Button } from '@/components/ui/button';
import { Table } from '@/components/ui/table';
import { useActions } from '@/hooks/useActions';

export function ActionList() {
  const { data, isLoading } = useActions();
  
  if (isLoading) return <Skeleton />;
  
  return (
    <div>
      <div className="flex justify-between mb-4">
        <h1>Actions</h1>
        <Button onClick={handleCreate}>Create Action</Button>
      </div>
      
      <Table>
        {/* Table content */}
      </Table>
    </div>
  );
}

Tailwind CSS

  • Purpose: Utility-first CSS framework
  • Why: Fast styling, consistent design system, small production bundle
  • Configuration:
    • Custom color palette matching Attune brand
    • Dark mode support
    • Responsive breakpoints
    • Custom animations for loading states

Routing

React Router v6

  • Purpose: Client-side routing and navigation
  • Why: Standard React routing solution, type-safe with TypeScript
  • Features:
    • Nested routes
    • Lazy loading for code splitting
    • Protected routes
    • URL parameter handling

Route Structure:

// src/router.tsx
import { createBrowserRouter } from 'react-router-dom';

export const router = createBrowserRouter([
  {
    path: '/login',
    element: <LoginPage />,
  },
  {
    path: '/',
    element: <ProtectedRoute><Layout /></ProtectedRoute>,
    children: [
      { index: true, element: <DashboardPage /> },
      
      // Packs
      { path: 'packs', element: <PackListPage /> },
      { path: 'packs/:ref', element: <PackDetailPage /> },
      
      // Actions
      { path: 'actions', element: <ActionListPage /> },
      { path: 'actions/:ref', element: <ActionDetailPage /> },
      { path: 'actions/:ref/edit', element: <ActionEditPage /> },
      
      // Rules
      { path: 'rules', element: <RuleListPage /> },
      { path: 'rules/:ref', element: <RuleDetailPage /> },
      
      // Workflows
      { path: 'workflows', element: <WorkflowListPage /> },
      { path: 'workflows/:ref', element: <WorkflowDetailPage /> },
      { path: 'workflows/:ref/edit', element: <WorkflowEditorPage /> },
      
      // Executions
      { path: 'executions', element: <ExecutionListPage /> },
      { path: 'executions/:id', element: <ExecutionDetailPage /> },
      
      // Events & Enforcements
      { path: 'events', element: <EventListPage /> },
      { path: 'enforcements', element: <EnforcementListPage /> },
      
      // Inquiries
      { path: 'inquiries', element: <InquiryListPage /> },
      { path: 'inquiries/:id', element: <InquiryDetailPage /> },
      
      // Settings
      { path: 'settings', element: <SettingsPage /> },
      { path: 'settings/secrets', element: <SecretsPage /> },
    ],
  },
]);

State Management

Approach: Minimal global state, prefer server state (React Query) and component state

Global State (if needed):

  • Zustand: Lightweight state management for truly global UI state
  • Use cases:
    • Theme preference (light/dark)
    • Sidebar collapsed/expanded state
    • User preferences
    • Command palette open/closed
// src/stores/uiStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UIState {
  theme: 'light' | 'dark';
  sidebarCollapsed: boolean;
  toggleTheme: () => void;
  toggleSidebar: () => void;
}

export const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      theme: 'dark',
      sidebarCollapsed: false,
      toggleTheme: () => set((state) => ({ 
        theme: state.theme === 'light' ? 'dark' : 'light' 
      })),
      toggleSidebar: () => set((state) => ({ 
        sidebarCollapsed: !state.sidebarCollapsed 
      })),
    }),
    { name: 'attune-ui-store' }
  )
);

Project Structure

attune-web/
├── public/
│   ├── favicon.ico
│   └── logo.svg
├── src/
│   ├── api/                      # OpenAPI generated code
│   │   ├── models/              # TypeScript interfaces for all DTOs
│   │   ├── services/            # API service classes
│   │   └── core/                # Axios client configuration
│   ├── components/
│   │   ├── ui/                  # shadcn/ui base components
│   │   ├── layout/              # Layout components (Sidebar, Header, etc.)
│   │   ├── actions/             # Action-related components
│   │   ├── rules/               # Rule-related components
│   │   ├── workflows/           # Workflow editor and viewer
│   │   ├── executions/          # Execution monitoring
│   │   ├── events/              # Event stream
│   │   └── common/              # Shared components
│   ├── hooks/                   # Custom React hooks
│   │   ├── useActions.ts
│   │   ├── useRules.ts
│   │   ├── useWorkflows.ts
│   │   ├── useExecutions.ts
│   │   └── useWebSocket.ts
│   ├── contexts/                # React contexts
│   │   └── AuthContext.tsx
│   ├── stores/                  # Zustand stores (minimal)
│   │   └── uiStore.ts
│   ├── pages/                   # Page components
│   │   ├── DashboardPage.tsx
│   │   ├── LoginPage.tsx
│   │   ├── actions/
│   │   ├── rules/
│   │   ├── workflows/
│   │   └── executions/
│   ├── websocket/               # WebSocket client
│   │   └── client.ts
│   ├── utils/                   # Utility functions
│   │   ├── formatters.ts
│   │   ├── validators.ts
│   │   └── yaml.ts
│   ├── types/                   # Additional TypeScript types
│   ├── router.tsx               # React Router configuration
│   ├── App.tsx                  # Root component
│   └── main.tsx                 # Entry point
├── .env.development             # Dev environment variables
├── .env.production              # Prod environment variables
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── tailwind.config.js

Development Workflow

Initial Setup

# Create React + TypeScript project with Vite
npm create vite@latest attune-web -- --template react-ts
cd attune-web

# Install core dependencies
npm install react-router-dom @tanstack/react-query axios
npm install @monaco-editor/react reactflow zustand js-yaml

# Install UI dependencies
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Install dev dependencies
npm install -D @types/node openapi-typescript-codegen

# Initialize shadcn/ui
npx shadcn-ui@latest init

Generate API Client

# Generate TypeScript client from OpenAPI spec
# Run this whenever the API changes
npm run generate:api

package.json script:

{
  "scripts": {
    "generate:api": "openapi-typescript-codegen --input http://localhost:8080/api-spec/openapi.json --output ./src/api --client axios --useOptions"
  }
}

Development Server

# Start dev server with HMR
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

Environment Variables

.env.development:

VITE_API_URL=http://localhost:8080
VITE_WS_URL=ws://localhost:8081
VITE_LOG_LEVEL=debug

.env.production:

VITE_API_URL=https://api.attune.example.com
VITE_WS_URL=wss://notifications.attune.example.com
VITE_LOG_LEVEL=error

Form Management and Entity Editability

Pack-Based vs UI-Configurable Components

Attune uses a pack-based architecture where most automation components are defined as code and bundled into packs. The Web UI must respect these architectural constraints when providing forms.

Code-Based Components (NOT UI-Editable)

Actions:

  • Implemented as executable code (Python, Node.js, Shell)
  • Registered when a pack is loaded/installed
  • No create/edit forms in Web UI
  • Managed through pack lifecycle (install, update, uninstall)
  • Rationale: Security, performance, code quality, testing requirements

Sensors:

  • Implemented as executable code with event monitoring logic
  • Registered when a pack is loaded/installed
  • No create/edit forms in Web UI
  • Managed through pack lifecycle
  • Rationale: Require event loop integration, complex dependencies, safety

Mixed Model Components

Triggers:

  • Pack-based triggers: Registered with system packs (e.g., slack.message_received)
    • NOT UI-editable - defined in pack manifest
  • Ad-hoc triggers: For custom integrations without code
    • UI-editable via trigger form (future feature)
    • Only for ad-hoc packs (system: false)
    • Define parameters schema and payload schema

Always UI-Configurable Components

Rules:

  • Connect triggers to actions with criteria and parameters
  • No code execution, just data mapping
  • Full CRUD operations via Web UI
  • Users need flexibility to change business logic

Packs (Ad-Hoc):

  • User-created packs for custom automation
  • Full CRUD operations via Web UI
  • Define configuration schema (JSON Schema format)
  • No code required

Workflows (Future):

  • Multi-step automation sequences
  • Visual workflow editor (React Flow)
  • Workflow actions (special configurable actions)

Form Implementation Guidelines

Required Forms:

  1. RuleForm (/rules/new, /rules/:id/edit) - Implemented
  2. PackForm (/packs/new, /packs/:name/edit) - Implemented
  3. 🔄 TriggerForm (/triggers/new, /triggers/:id/edit) - Future
    • Only for ad-hoc packs
    • Validate pack is non-system before allowing trigger creation
  4. 🔄 WorkflowForm (/workflows/new, /workflows/:ref/edit) - Future

NOT Required:

  • ActionForm - Actions are code-based
  • SensorForm - Sensors are code-based

Form Validation Strategy

Client-Side Validation:

  • Required field checks
  • Format validation (names, versions, JSON syntax)
  • JSON Schema validation for configuration schemas
  • Real-time error display with field-level messages

Server-Side Validation:

  • API error capture and display
  • Generic error handling for network failures
  • Field-specific errors when available

Form State Management

// Example: RuleForm component structure
function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
  const isEditing = !!rule;
  
  // Local form state
  const [packId, setPackId] = useState(rule?.pack_id || 0);
  const [triggerId, setTriggerId] = useState(rule?.trigger_id || 0);
  const [actionId, setActionId] = useState(rule?.action_id || 0);
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  // Data fetching
  const { data: packs } = usePacks();
  const { data: triggers } = usePackTriggers(selectedPackName);
  const { data: actions } = usePackActions(selectedPackName);
  
  // Mutations
  const createRule = useCreateRule();
  const updateRule = useUpdateRule();
  
  // Validation and submission
  const validateForm = () => { /* ... */ };
  const handleSubmit = async (e) => { /* ... */ };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Key Patterns:

  • Cascading dropdowns (pack → triggers/actions)
  • Immutable fields when editing (pack, trigger, action IDs)
  • JSON editors with syntax validation
  • Optimistic UI updates after mutations
  • Auto-navigation after successful creation

Entity List Pages with Create Buttons

Pattern: List pages should have a prominent "Create" button when entity creation is allowed.

Implemented:

  • Rules list page: "Create Rule" button → /rules/new
  • Packs list page: "Register Pack" button → /packs/new

Should NOT Have Create Buttons:

  • Actions list page - Actions are code-based
  • Sensors list page - Sensors are code-based

Future:

  • 🔄 Triggers list page: "Create Trigger" button (only shows for ad-hoc packs)
  • 🔄 Workflows list page: "Create Workflow" button

See docs/pack-management-architecture.md for detailed architectural rationale.


Key Features Implementation

Dashboard

  • Real-time metrics: Active executions, success/failure rates, event throughput
  • Recent activity: Latest executions, events, and inquiries
  • Quick actions: Common tasks (run workflow, create rule)
  • System health: Service status indicators

Pack Management

  • List view: All packs with search and filters
  • Detail view: Pack info, contained actions/rules/workflows
  • Create/Edit: Form for pack metadata
  • Sync workflows: Trigger workflow sync from pack directory

Action Management

  • List view: Searchable, filterable table of actions
  • Detail view: Action parameters, metadata, associated rules
  • Create/Edit: Form with parameter schema editor
  • Test runner: Execute action with test parameters

Rule Management

  • List view: All rules with enable/disable toggle
  • Detail view: Trigger, action, parameter mapping, criteria
  • Create/Edit: Visual rule builder with expression editor
  • Testing: Test rule against sample event payload

Workflow Management

  • List view: Workflows with status, tags, last execution
  • Visual editor: React Flow-based workflow designer
  • YAML editor: Monaco editor with validation
  • Dual mode: Switch between visual and code editing
  • Execution history: View past executions with drill-down

Execution Monitoring

  • Live dashboard: Real-time execution updates via WebSocket
  • Filterable list: By status, action, pack, time range
  • Detail view: Full execution context, logs, parent/child relationships
  • Retry/Cancel: Actions on executions
  • Log streaming: Real-time log output for running executions

Event Stream

  • Live feed: Real-time event stream
  • Filters: By trigger, status, time range
  • Detail view: Event payload, resulting enforcements
  • Replay: Re-evaluate rules against past events

Inquiry Management

  • Notification center: Pending inquiries requiring action
  • Response interface: Form for inquiry responses
  • History: Past inquiries with responses

Performance Optimizations

  1. Code Splitting: Lazy load routes with React.lazy()
  2. Bundle Optimization: Tree shaking, minification via Vite
  3. Image Optimization: WebP format, lazy loading
  4. Query Caching: Intelligent cache times based on data volatility
  5. Virtual Scrolling: For large lists (executions, events)
  6. Debounced Search: Avoid excessive API calls
  7. Optimistic Updates: Immediate UI feedback on mutations
  8. Prefetching: Prefetch likely next pages on hover

Testing Strategy

Unit Tests

  • Component logic with React Testing Library
  • Hook behavior with @testing-library/react-hooks
  • Utility functions with Jest

Integration Tests

  • User flows (login, create workflow, monitor execution)
  • API client mocking with MSW (Mock Service Worker)
  • Form validation and submission

E2E Tests

  • Critical paths with Playwright
  • Cross-browser testing
  • Accessibility testing with axe-core

Deployment

Build Output

npm run build
# Outputs to dist/ directory

Static Hosting

  • Deploy dist/ to any static host (Nginx, Cloudflare Pages, Vercel, Netlify)
  • Configure routing: redirect all requests to index.html for SPA routing

Docker Container

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf:

server {
  listen 80;
  root /usr/share/nginx/html;
  index index.html;

  # SPA routing: redirect all to index.html
  location / {
    try_files $uri $uri/ /index.html;
  }

  # Cache static assets
  location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}

Security Considerations

  1. XSS Prevention: React's automatic escaping, CSP headers
  2. Token Storage: Access token in memory (preferred) or localStorage with caution
  3. API Communication: HTTPS only in production
  4. Input Validation: Client-side validation + server-side enforcement
  5. CORS: Proper CORS configuration on API service
  6. CSP Headers: Content Security Policy to prevent injection attacks

Accessibility

  1. Semantic HTML: Proper heading hierarchy, landmarks
  2. ARIA Labels: For interactive elements and dynamic content
  3. Keyboard Navigation: All features accessible via keyboard
  4. Focus Management: Clear focus indicators, focus trapping in modals
  5. Screen Reader Support: Announcements for dynamic updates
  6. Color Contrast: WCAG AA compliance minimum

Browser Support

  • Modern Browsers: Chrome, Firefox, Safari, Edge (latest 2 versions)
  • No IE Support: Modern JavaScript and CSS features used

Future Enhancements

  1. Progressive Web App: Offline support, install prompt
  2. Internationalization: i18n support with react-i18next
  3. Advanced Workflow Editor: Template library, drag-from-palette
  4. Collaboration: Multi-user editing indicators
  5. Telemetry: Usage analytics and error tracking
  6. Mobile Responsive: Optimized layouts for tablets and phones
  7. Command Palette: Keyboard-driven navigation (Cmd+K)
  8. Export/Import: Workflow and pack export in various formats

Documentation for Developers

All developers working on the web UI should familiarize themselves with:

  1. This architecture document
  2. OpenAPI specification at /api-spec/openapi.json
  3. React Query documentation for data fetching patterns
  4. shadcn/ui component documentation
  5. React Flow documentation for workflow editor

Troubleshooting Common Issues

API Client Out of Sync

Problem: TypeScript errors after API changes
Solution: Regenerate client with npm run generate:api

WebSocket Not Connecting

Problem: Real-time updates not working
Solution: Check VITE_WS_URL, verify notifier service is running

Authentication Loops

Problem: Constant redirects to login
Solution: Check token expiry, verify refresh token mechanism

Slow Initial Load

Problem: Large bundle size
Solution: Review code splitting, check for unnecessary dependencies in bundle


This architecture provides a solid foundation for building a professional, performant, and maintainable web interface for the Attune automation platform.