Web Frontend

🌐 SolidJS-based progressive web application for PiSovereign

The web frontend provides a modern chat interface for interacting with PiSovereign through any browser. It is built with SolidJS and embedded directly into the Rust binary at compile time via rust-embed.

Table of Contents


Overview

PiSovereign’s web frontend is a single-page application (SPA) that communicates with the backend via REST and Server-Sent Events (SSE). Key design goals:

  • Zero external hosting β€” Assets are embedded in the Rust binary, no separate web server needed
  • Offline-capable β€” PWA with service worker for offline resilience
  • Privacy-first β€” No external CDNs, fonts, or analytics; everything is self-contained
  • Lightweight β€” ~200 KB production bundle with code splitting

Technology Stack

TechnologyVersionPurpose
SolidJS1.9.xReactive UI framework
SolidJS Router0.15.xClient-side routing
TypeScript5.7Type safety (strict mode)
Vite6.xBuild tool & dev server
Tailwind CSS4.xUtility-first styling
Vitest3.xUnit & component testing
vite-plugin-pwa1.xService worker generation

Architecture

Directory Structure

crates/presentation_web/
β”œβ”€β”€ Cargo.toml              # Rust crate manifest
β”œβ”€β”€ dist/                   # Vite build output (gitignored)
β”œβ”€β”€ frontend/               # SolidJS source code
β”‚   β”œβ”€β”€ index.html          # HTML entry point
β”‚   β”œβ”€β”€ package.json        # Node dependencies
β”‚   β”œβ”€β”€ tsconfig.json       # TypeScript configuration
β”‚   β”œβ”€β”€ vite.config.ts      # Vite build configuration
β”‚   β”œβ”€β”€ vitest.config.ts    # Test configuration
β”‚   └── src/
β”‚       β”œβ”€β”€ index.tsx        # Application entry point
β”‚       β”œβ”€β”€ app.tsx          # Root component with router
β”‚       β”œβ”€β”€ api/             # REST & SSE API clients
β”‚       β”œβ”€β”€ components/      # Reusable UI components
β”‚       β”‚   └── ui/          # Base UI primitives
β”‚       β”œβ”€β”€ hooks/           # Reactive hooks
β”‚       β”œβ”€β”€ lib/             # Utilities (cn, format, sanitize)
β”‚       β”œβ”€β”€ pages/           # Route page components
β”‚       β”œβ”€β”€ stores/          # Global state stores
β”‚       └── types/           # TypeScript type definitions
└── src/                    # Rust source code
    β”œβ”€β”€ lib.rs              # Crate root
    β”œβ”€β”€ assets.rs           # rust-embed asset struct
    β”œβ”€β”€ csp.rs              # Content Security Policy
    β”œβ”€β”€ handler.rs          # Static file handler with caching
    └── routes.rs           # SPA router & axum integration

Component Architecture

The frontend follows a layered component architecture:

Pages (routes)
  └── Composed Components (chat, settings panels)
        └── UI Primitives (Button, Card, Modal, Badge, Spinner)
              └── Utility Functions (cn, format, sanitize)

Pages are lazy-loaded via solid-router for code splitting:

  • / β€” Chat interface (main interaction view)
  • /settings β€” Application settings
  • /commands β€” Available bot commands
  • /memories β€” Memory inspection
  • /audit β€” Audit log viewer
  • /health β€” System health dashboard

UI Primitives (components/ui/) are unstyled, composable building blocks:

  • Button β€” With variants (default, outline, ghost, destructive) and sizes
  • Card β€” Container with header/content/footer slots
  • Modal β€” Dialog with focus trap and backdrop
  • Badge β€” Status indicators with color variants
  • Spinner β€” Loading indicators

State Management

Global state uses SolidJS signals organized into stores:

StorePurpose
auth.storeAuthentication state, token management
chat.storeConversations, messages, active chat
theme.storeDark/light mode preference
toast.storeNotification queue

Stores are accessed via the StoreProvider context, available throughout the component tree.

API Client Layer

The api/ directory contains typed REST clients:

ClientEndpointPurpose
client.tsβ€”Base HTTP client with auth headers
chat.api.ts/api/v1/chatSend messages, SSE streaming
health.api.ts/api/v1/healthSystem health status
memories.api.ts/api/v1/memoriesMemory CRUD
audit.api.ts/api/v1/auditAudit log queries
commands.api.ts/api/v1/commandsBot command listing
settings.api.ts/api/v1/settingsUser preferences

All API calls go through client.ts, which handles:

  • Bearer token injection from the auth store
  • Base URL resolution (same origin in production, proxy in dev)
  • JSON serialization/deserialization
  • Error response mapping

Development

Prerequisites

  • Node.js β‰₯ 22 (LTS recommended)
  • npm β‰₯ 10

Getting Started

# Install dependencies
just web-install

# Start development server with hot reload
just web-dev

The Vite dev server starts on http://localhost:5173 and proxies API requests to the backend at http://localhost:3000.

Available Commands

All frontend tasks are available via just:

CommandDescription
just web-installInstall npm dependencies
just web-buildProduction build β†’ dist/
just web-devStart Vite dev server with HMR
just web-lintRun ESLint checks
just web-lint-fixAuto-fix ESLint issues
just web-fmtFormat with Prettier
just web-testRun Vitest test suite
just web-test-coverageRun tests with coverage report
just web-typecheckTypeScript type checking

Development Workflow

  1. Start the backend: just run or cargo run
  2. Start the frontend dev server: just web-dev
  3. Open http://localhost:5173 in your browser
  4. Edit SolidJS components β€” changes are reflected instantly via HMR

The Vite dev server proxies /api/* requests to localhost:3000, so you get the full API available during development.

Build & Deployment

Production Build

just web-build

This runs vite build which outputs optimized assets to crates/presentation_web/dist/. The build:

  • Tree-shakes unused code
  • Minifies JS/CSS
  • Adds content hashes to filenames for cache busting
  • Generates PWA service worker
  • Code-splits routes for lazy loading
  • Outputs ~200 KB total (gzipped ~60 KB)

Rust Integration

The presentation_web crate embeds the dist/ directory at compile time using rust-embed:

#[derive(Embed)]
#[folder = "dist/"]
pub struct FrontendAssets;

The Rust handler layer provides:

  • Content-type detection β€” MIME types based on file extension
  • Cache control β€” Immutable caching for hashed assets, no-cache for HTML
  • ETag support β€” Conditional requests via If-None-Match
  • CSP headers β€” Content Security Policy for XSS protection
  • SPA fallback β€” Unknown routes serve index.html for client-side routing

Important: You must run just web-build before cargo build so the dist/ directory is populated. The Docker build handles this automatically.

Docker Build

The Dockerfile uses a multi-stage build:

# Stage 1: Build frontend
FROM node:22-alpine AS frontend-builder
COPY crates/presentation_web/frontend/ .
RUN npm ci && npm run build

# Stage 2: Build Rust binary
FROM rust:1.93-slim-bookworm AS builder
COPY --from=frontend-builder /app/dist/ crates/presentation_web/dist/
RUN cargo build --release

# Stage 3: Runtime
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/pisovereign .

This ensures the frontend is always built fresh and embedded into the binary.

Testing

Unit & Component Tests

Tests use Vitest with @solidjs/testing-library for component tests and MSW (Mock Service Worker) for API mocking.

# Run all tests
just web-test

# Run with coverage
just web-test-coverage

Test structure mirrors the source layout:

frontend/src/
β”œβ”€β”€ lib/__tests__/          # Utility tests (cn, format, sanitize)
β”œβ”€β”€ stores/__tests__/       # Store logic tests
β”œβ”€β”€ api/__tests__/          # API client tests
└── components/ui/__tests__/ # Component rendering tests

Current coverage: 93 tests across utilities, stores, API clients, and UI components.

End-to-End Tests (Playwright)

The project includes Playwright-based E2E journey tests that simulate real user interactions against a live application instance on localhost:3000. These tests cover every page, CRUD operation, and user action in the frontend.

Setup

# Install Playwright browsers (one-time)
just web-e2e-install

# Ensure the Docker stack is running
just docker-up

Running E2E Tests

# Run all journey tests
just web-e2e

# Run with interactive UI (for debugging)
just web-e2e-ui

# View the last HTML report
just web-e2e-report

Skipping LLM-Dependent Tests

Tests tagged @llm (Chat, Agentic) require Ollama to be running. To skip them:

cd crates/presentation_web/frontend && npx playwright test --grep-invert @llm

Architecture

frontend/e2e/
β”œβ”€β”€ global-setup.ts              # Authenticates once, saves session cookie
β”œβ”€β”€ fixtures/
β”‚   └── auth.fixture.ts          # Pre-authenticated page fixture
β”œβ”€β”€ reporters/
β”‚   └── bugreport.reporter.ts    # Auto-generates bug reports on failure
β”œβ”€β”€ helpers/
β”‚   β”œβ”€β”€ navigation.helper.ts     # Sidebar navigation, page-load utilities
β”‚   └── form.helper.ts           # Form fills, modal helpers, test IDs
└── journeys/
    β”œβ”€β”€ auth.journey.spec.ts         # Login, logout, session persistence
    β”œβ”€β”€ dashboard.journey.spec.ts    # Stat cards, quick actions, sections
    β”œβ”€β”€ chat.journey.spec.ts         # Send message, SSE streaming (@llm)
    β”œβ”€β”€ conversations.journey.spec.ts # List, search, delete conversations
    β”œβ”€β”€ commands.journey.spec.ts     # Parse, execute, catalog CRUD
    β”œβ”€β”€ approvals.journey.spec.ts    # List, approve, deny requests
    β”œβ”€β”€ contacts.journey.spec.ts     # Full CRUD + search
    β”œβ”€β”€ calendar.journey.spec.ts     # Views, event CRUD, date navigation
    β”œβ”€β”€ tasks.journey.spec.ts        # Task CRUD, filters, completion
    β”œβ”€β”€ kanban.journey.spec.ts       # Board columns, filter buttons
    β”œβ”€β”€ memory.journey.spec.ts       # Memory CRUD, search, decay, stats
    β”œβ”€β”€ agentic.journey.spec.ts      # Multi-agent task lifecycle (@llm)
    β”œβ”€β”€ mailing.journey.spec.ts      # Email list, refresh, mark read
    └── system.journey.spec.ts       # Status, models, health checks

Writing New Journey Tests

Journey tests follow a consistent pattern using test.step() for structured reproduction steps:

import { test, expect } from '../fixtures/auth.fixture';

test.describe('Feature Journey', () => {
  test('complete user flow', async ({ page }) => {
    await test.step('navigate to the page', async () => {
      await page.goto('/feature');
      await page.waitForLoadState('networkidle');
    });

    await test.step('perform user action', async () => {
      await page.locator('button:has-text("Action")').click();
      await expect(page.locator('text=Result')).toBeVisible();
    });
  });
});

Key conventions:

  • File naming: {feature}.journey.spec.ts
  • Test steps: Use test.step() β€” these feed the bugreport reporter for clear reproduction steps
  • Data cleanup: Create test data with unique IDs (testId() helper) and clean up in afterAll or at the end of the test
  • Timeouts: Use generous timeouts (60s) for LLM/SSE-dependent tests and tag them with @llm
  • Resilience: Use .catch(() => false) for optional UI elements that may or may not exist depending on backend state

Automatic Bug Reports

When a test fails, the custom BugreportReporter writes a detailed markdown file to bugreports/:

  • Title and test metadata (file, line, browser, duration)
  • Steps to reproduce extracted from test.step() annotations
  • Error message and stack trace
  • Screenshot paths (captured on failure)
  • Environment details (OS, Node.js version)

Bug reports are named YYYY-MM-DD-e2e-{test-slug}.md for chronological ordering.

Code Quality

The project enforces strict code quality standards:

  • TypeScript strict mode β€” All strict checks enabled, including exactOptionalPropertyTypes
  • ESLint β€” SolidJS-specific rules + TypeScript checks (0 errors required)
  • Prettier β€” Consistent formatting
  • Pre-commit checks β€” just pre-commit runs lint, typecheck, and tests

Quality gates are integrated into the just quality and just pre-commit recipes, which run both frontend and backend checks together.

PWA Support

The frontend is a Progressive Web App with:

  • Service Worker β€” Generated by vite-plugin-pwa using Workbox
  • Offline support β€” Cached assets served when offline
  • Installable β€” Add to home screen on mobile devices
  • Web manifest β€” App name, icons, theme colors defined in manifest.webmanifest

The service worker uses a cache-first strategy for static assets and network-first for API calls.

Styling

Styling uses Tailwind CSS v4 with a custom design system:

  • CSS custom properties for theming (dark/light mode)
  • Utility classes for layout, spacing, typography
  • cn() helper β€” Merges Tailwind classes with conflict resolution via tailwind-merge + clsx
  • No external CSS frameworks β€” Everything is built from Tailwind utilities

The color palette follows a navy/slate theme matching PiSovereign’s brand identity.

Security

The embedded frontend includes several security measures:

  • Content Security Policy (CSP) β€” Restricts script sources, style sources, and connections
  • No inline scripts β€” All JavaScript is loaded from hashed asset files
  • Same-origin API calls β€” No cross-origin requests by design
  • No external dependencies at runtime β€” Fonts, icons, and all assets are self-hosted
  • Auth token handling β€” Tokens stored in memory (SolidJS signals), not localStorage

See the Security Hardening guide for production deployment recommendations.