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
- Technology Stack
- Architecture
- Development
- Build & Deployment
- Testing
- Code Quality
- PWA Support
- Styling
- Security
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
| Technology | Version | Purpose |
|---|---|---|
| SolidJS | 1.9.x | Reactive UI framework |
| SolidJS Router | 0.15.x | Client-side routing |
| TypeScript | 5.7 | Type safety (strict mode) |
| Vite | 6.x | Build tool & dev server |
| Tailwind CSS | 4.x | Utility-first styling |
| Vitest | 3.x | Unit & component testing |
| vite-plugin-pwa | 1.x | Service 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 sizesCardβ Container with header/content/footer slotsModalβ Dialog with focus trap and backdropBadgeβ Status indicators with color variantsSpinnerβ Loading indicators
State Management
Global state uses SolidJS signals organized into stores:
| Store | Purpose |
|---|---|
auth.store | Authentication state, token management |
chat.store | Conversations, messages, active chat |
theme.store | Dark/light mode preference |
toast.store | Notification queue |
Stores are accessed via the StoreProvider context, available throughout the component tree.
API Client Layer
The api/ directory contains typed REST clients:
| Client | Endpoint | Purpose |
|---|---|---|
client.ts | β | Base HTTP client with auth headers |
chat.api.ts | /api/v1/chat | Send messages, SSE streaming |
health.api.ts | /api/v1/health | System health status |
memories.api.ts | /api/v1/memories | Memory CRUD |
audit.api.ts | /api/v1/audit | Audit log queries |
commands.api.ts | /api/v1/commands | Bot command listing |
settings.api.ts | /api/v1/settings | User 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:
| Command | Description |
|---|---|
just web-install | Install npm dependencies |
just web-build | Production build β dist/ |
just web-dev | Start Vite dev server with HMR |
just web-lint | Run ESLint checks |
just web-lint-fix | Auto-fix ESLint issues |
just web-fmt | Format with Prettier |
just web-test | Run Vitest test suite |
just web-test-coverage | Run tests with coverage report |
just web-typecheck | TypeScript type checking |
Development Workflow
- Start the backend:
just runorcargo run - Start the frontend dev server:
just web-dev - Open
http://localhost:5173in your browser - 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.htmlfor 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 inafterAllor 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-commitruns 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-pwausing 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 viatailwind-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.