# Silo Frontend Specification Current as of 2026-02-11. Documents the React + Vite + TypeScript frontend (migration from Go templates is complete). ## Overview The Silo web UI has been migrated from server-rendered Go templates to a React single-page application. The Go templates (~7,000 lines across 7 files) have been removed. The Go API server serves JSON at `/api/*` and the React SPA at `/`. **Stack**: React 19, React Router 7, Vite 6, TypeScript 5.7 **Theme**: Catppuccin Mocha (dark) via CSS custom properties **Styling**: Inline React styles using `React.CSSProperties` — no CSS modules, no Tailwind, no styled-components **State**: Local `useState` + custom hooks. No global state library (no Redux, Zustand, etc.) **Dependencies**: Minimal — only `react`, `react-dom`, `react-router-dom`. No axios, no tanstack-query. ## Migration Status | Phase | Issue | Title | Status | |-------|-------|-------|--------| | 1 | #7 | Scaffold React + Vite + TS, shared layout, auth, API client | Code complete | | 2 | #8 | Migrate Items page with UI improvements | Code complete | | 3 | #9 | Migrate Projects, Schemas, Settings, Login pages | Code complete | | 4 | #10 | Remove Go templates, Docker integration, cleanup | Complete | ## Architecture ``` Browser └── React SPA (served at /) ├── Vite dev server (development) → proxies /api/* to Go backend └── Static files in web/dist/ (production) → served by Go binary Go Server (silod) ├── /api/* JSON REST API ├── /login, /logout Session auth endpoints (form POST) ├── /auth/oidc OIDC redirect flow └── /* React SPA (NotFound handler serves index.html for client-side routing) ``` ### Auth Flow 1. React app loads, `AuthProvider` calls `GET /api/auth/me` 2. If 401 → render `LoginPage` (React form) 3. Login form POSTs `application/x-www-form-urlencoded` to `/login` (Go handler sets session cookie) 4. On success, `AuthProvider.refresh()` re-fetches `/api/auth/me`, user state populates, app renders 5. OIDC: link to `/auth/oidc` triggers Go-served redirect flow, callback sets session, user returns to app 6. API client auto-redirects to `/login` on any 401 response ### Public API Endpoint `GET /api/auth/config` — returns `{ oidc_enabled: bool, local_enabled: bool }` so the login page can conditionally show the OIDC button without hardcoding. ## File Structure ``` web/ ├── index.html ├── package.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── src/ ├── main.tsx Entry point, renders AuthProvider + BrowserRouter + App ├── App.tsx Route definitions, auth guard ├── api/ │ ├── client.ts fetch wrapper: get, post, put, del + ApiError class │ └── types.ts All TypeScript interfaces (272 lines) ├── context/ │ └── AuthContext.tsx AuthProvider with login/logout/refresh methods ├── hooks/ │ ├── useAuth.ts Context consumer hook │ ├── useFormDescriptor.ts Fetches form descriptor from /api/schemas/{name}/form (replaces useCategories) │ ├── useItems.ts Items fetching with search, filters, pagination, debounce │ └── useLocalStorage.ts Typed localStorage persistence hook ├── styles/ │ ├── theme.css Catppuccin Mocha CSS custom properties │ └── global.css Base element styles ├── components/ │ ├── AppShell.tsx Header nav + user info + │ ├── ContextMenu.tsx Reusable right-click positioned menu │ └── items/ Items page components (16 files) │ ├── ItemsToolbar.tsx Search, filters, layout toggle, action buttons │ ├── ItemTable.tsx Sortable table, column config, compact rows │ ├── ItemDetail.tsx 5-tab detail panel (Main, Properties, Revisions, BOM, Where Used) │ ├── MainTab.tsx Metadata display, project tags editor, file info │ ├── PropertiesTab.tsx Form/JSON dual-mode property editor │ ├── RevisionsTab.tsx Revision list, compare diff, status, rollback │ ├── BOMTab.tsx BOM table with inline CRUD, cost calculations │ ├── WhereUsedTab.tsx Parent assemblies table │ ├── SplitPanel.tsx Resizable horizontal/vertical layout container │ ├── FooterStats.tsx Fixed bottom bar with item counts │ ├── CreateItemPane.tsx In-pane create form with schema category properties │ ├── EditItemPane.tsx In-pane edit form │ ├── DeleteItemPane.tsx In-pane delete confirmation │ └── ImportItemsPane.tsx CSV upload with dry-run/import flow └── pages/ ├── LoginPage.tsx Username/password form + OIDC button ├── ItemsPage.tsx Orchestrator: toolbar, split panel, table, detail/CRUD panes ├── ProjectsPage.tsx Project CRUD with sortable table, in-pane forms ├── SchemasPage.tsx Schema browser with collapsible segments, enum value CRUD ├── SettingsPage.tsx Account info, API token management └── AuditPage.tsx Audit completeness (placeholder, expanded in Issue #5) ``` **Total**: ~40 source files, ~7,600 lines of TypeScript/TSX. ## Design System ### Theme Catppuccin Mocha dark theme. All colors referenced via CSS custom properties: | Token | Use | |-------|-----| | `--ctp-base` | Page background, input backgrounds | | `--ctp-mantle` | Header background | | `--ctp-surface0` | Card backgrounds, even table rows | | `--ctp-surface1` | Borders, dividers, hover states | | `--ctp-surface2` | Secondary button backgrounds | | `--ctp-text` | Primary text | | `--ctp-subtext0/1` | Secondary/muted text, labels | | `--ctp-overlay0` | Placeholder text | | `--ctp-mauve` | Brand accent, primary buttons, nav active | | `--ctp-blue` | Editor role badge, edit headers | | `--ctp-green` | Success banners, create headers | | `--ctp-red` | Errors, delete actions, danger buttons | | `--ctp-peach` | Part numbers, project codes, token prefixes | | `--ctp-teal` | Viewer role badge | | `--ctp-sapphire` | Links, collapsible toggles | | `--ctp-crust` | Dark text on colored backgrounds | ### Typography - Body: system font stack (Inter, -apple-system, etc.) - Monospace: JetBrains Mono (part numbers, codes, tokens) - Table cells: 0.85rem - Labels: 0.85rem, weight 500 - Table headers: 0.8rem, uppercase, letter-spacing 0.05em ### Component Patterns **Tables**: Inline styles, compact rows (28-32px), alternating `base`/`surface0` backgrounds, sortable headers with arrow indicators, right-click column config (Items page). **Forms**: In-pane forms (Infor ERP-style) — not modal overlays. Create/Edit/Delete forms render in the detail pane area with a colored header bar (green=create, blue=edit, red=delete). Cancel returns to previous view. **Cards**: `surface0` background, `0.75rem` border radius, `1.5rem` padding. **Buttons**: Primary (`mauve` bg, `crust` text), secondary (`surface1` bg), danger (`red` bg or translucent red bg with red text). **Errors**: Red text with translucent red background banner, `0.4rem` border radius. **Role badges**: Colored pill badges — admin=mauve, editor=blue, viewer=teal. ## Page Specifications ### Items Page (completed in #8) The most complex page. Master-detail layout with resizable split panel. **Toolbar**: Debounced search (300ms) with scope toggle (All/PN/Description), type and project filter dropdowns, layout toggle (horizontal/vertical), export/import/create buttons. **Table**: 7 configurable columns (part_number, item_type, description, revision, projects, created, actions). Visibility stored per layout mode in localStorage. Right-click header opens ContextMenu with checkboxes. Compact rows, zebra striping, click to select. **Detail panel**: 5 tabs — Main (metadata + project tags + file info), Properties (form/JSON editor, save creates revision), Revisions (compare, status management, rollback), BOM (inline CRUD, cost calculations, CSV export), Where Used (parent assemblies). **CRUD panes**: In-pane forms for Create (schema category properties, project tags), Edit (basic fields), Delete (confirmation), Import (CSV upload with dry-run). **Footer**: Fixed 28px bottom bar showing Total | Parts | Assemblies | Documents counts, reactive to filters. **State**: `PaneMode` discriminated union manages which pane is shown. `useItems` hook handles fetching, search, filters, pagination. `useLocalStorage` persists layout and column preferences. ### Projects Page (completed in #9) Sortable table with columns: Code, Name, Description, Items (count fetched per project), Created, Actions. **CRUD**: In-pane forms above the table. Create requires code (2-10 chars, auto-uppercase), name, description. Edit allows name and description changes. Delete shows confirmation with project code. **Navigation**: Click project code navigates to Items page with `?project=CODE` filter. **Permissions**: Create/Edit/Delete buttons only visible to editor/admin roles. ### Schemas Page (completed in #9) Schema cards with collapsible segment details. Each schema shows name, description, format string, version, and example part numbers. **Segments**: Expandable list showing segment name, type badge, description. Enum segments include a values table with code and description columns. **Enum CRUD**: Inline table operations — add row at bottom, edit replaces the row, delete highlights the row with confirmation. All operations call `POST/PUT/DELETE /api/schemas/{name}/segments/{segment}/values/{code}`. ### Settings Page (completed in #9) Two cards: **Account**: Read-only grid showing username, display name, email, auth source, role (with colored badge). Data from `useAuth()` context. **API Tokens**: Create form (name input + button), one-time token display in green banner with copy-to-clipboard, token list table (name, prefix, created, last used, expires, revoke). Revoke has inline confirm step. Uses `GET/POST/DELETE /api/auth/tokens`. ### Login Page (completed in #9) Standalone centered card (no AppShell). Username/password form, OIDC button shown conditionally based on `GET /api/auth/config`. Error messages in red banner. Submit calls `AuthContext.login()` which POSTs form data to `/login` then re-fetches the user. ### Audit Page (placeholder) Basic table showing audit completeness data from `GET /api/audit/completeness`. Will be expanded as part of Issue #5 (Component Audit UI with completeness scoring and inline editing). ## API Client `web/src/api/client.ts` — thin wrapper around `fetch`: - Always sends `credentials: 'include'` for session cookies - Always sets `Content-Type: application/json` - 401 responses redirect to `/login` - Non-OK responses parsed as `{ error, message }` and thrown as `ApiError` - 204 responses return `undefined` - Exports: `get()`, `post()`, `put()`, `del()` ## Type Definitions `web/src/api/types.ts` — 272 lines covering all API response and request shapes: **Core models**: User, Item, Project, Schema, SchemaSegment, Revision, BOMEntry **Audit**: AuditFieldResult, AuditItemResult, AuditSummary, AuditCompletenessResponse **Search**: FuzzyResult (extends Item with score) **BOM**: WhereUsedEntry, AddBOMEntryRequest, UpdateBOMEntryRequest **Items**: CreateItemRequest, UpdateItemRequest, CreateRevisionRequest **Projects**: CreateProjectRequest, UpdateProjectRequest **Schemas**: CreateSchemaValueRequest, UpdateSchemaValueRequest, PropertyDef, PropertySchema **Auth**: AuthConfig, ApiToken, ApiTokenCreated **Revisions**: RevisionComparison **Import**: CSVImportResult, CSVImportError **Errors**: ErrorResponse ## Completed Work ### Issue #10: Remove Go Templates + Docker Integration -- COMPLETE Completed in commit `50923cf`. All Go templates deleted, `web.go` handler removed, SPA serves at `/` via `NotFound` handler with `index.html` fallback. `build/package/Dockerfile` added. ### Remaining Work ### Issue #5: Component Audit UI (future) The Audit page will be expanded with completeness scoring, inline editing, tier filtering, and category breakdowns. This will be built natively in React using the patterns established in the migration. ## Development ```bash # Install dependencies cd web && npm install # Dev server (proxies /api/* to Go backend on :8080) npm run dev # Type check npx tsc --noEmit # Production build npm run build ``` Vite dev server runs on port 5173 with proxy config in `vite.config.ts` forwarding `/api/*`, `/login`, `/logout`, `/auth/*` to the Go backend. ## Conventions - **No modals for CRUD** — use in-pane forms (Infor ERP-style pattern) - **No shared component library extraction** until a pattern repeats 3+ times - **Inline styles only** — all styling via `React.CSSProperties` objects, using Catppuccin CSS variables - **No class components** — functional components with hooks only - **Permission checks**: derive `isEditor` from `user.role` in each page, conditionally render write actions - **Error handling**: try/catch with error state, display in red banners inline - **Data fetching**: `useEffect` + API client on mount, loading/error/data states - **Persistence**: `useLocalStorage` hook for user preferences (layout mode, column visibility) ## New Frontend Tasks # CreateItemPane — Schema-Driven Dynamic Form **Date**: 2026-02-10 **Scope**: `CreateItemPane.tsx` renders a dynamic form driven entirely by the form descriptor API (`GET /api/schemas/{name}/form`). All field groups, field types, widgets, and category-specific fields are defined in YAML and resolved server-side. **Parent**: Items page (`ItemsPage.tsx`) — renders in the detail pane area per existing in-pane CRUD pattern. --- ## Layout Single-column scrollable form with a green header bar. Field groups are rendered dynamically from the form descriptor. Category-specific field groups appear after global groups when a category is selected. ``` ┌──────────────────────────────────────────────────────────────────────┐ │ Header: "New Item" [green bar] Cancel │ Create │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ Category * [Domain buttons: F C R S E M T A P X] │ │ [Subcategory search + filtered list] │ │ │ │ ── Identity ────────────────────────────────────────────────────── │ │ [Type * (auto-derived from category)] [Description ] │ │ │ │ ── Sourcing ────────────────────────────────────────────────────── │ │ [Sourcing Type v] [Manufacturer] [MPN] [Supplier] [SPN] │ │ [Sourcing Link] │ │ │ │ ── Cost & Lead Time ────────────────────────────────────────────── │ │ [Standard Cost $] [Lead Time Days] [Min Order Qty] │ │ │ │ ── Status ──────────────────────────────────────────────────────── │ │ [Lifecycle Status v] [RoHS Compliant ☐] [Country of Origin] │ │ │ │ ── Details ─────────────────────────────────────────────────────── │ │ [Long Description ] │ │ [Projects: [tag][tag] type to search... ] │ │ [Notes ] │ │ │ │ ── Fastener Specifications (category-specific) ─────────────────── │ │ [Material] [Finish] [Thread Size] [Head Type] [Drive Type] ... │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` ## Data Source — Form Descriptor API All form structure is fetched from `GET /api/schemas/kindred-rd/form`, which returns: - `category_picker`: Multi-stage picker config (domain → subcategory) - `item_fields`: Definitions for item-level fields (description, item_type, sourcing_type, etc.) - `field_groups`: Ordered groups with resolved field metadata (Identity, Sourcing, Cost, Status, Details) - `category_field_groups`: Per-category-prefix groups (e.g., Fastener Specifications for `F` prefix) - `field_overrides`: Widget hints (currency, url, select, checkbox) The YAML schema (`schemas/kindred-rd.yaml`) is the single source of truth. Adding a new field or category in YAML propagates to all clients with no code changes. ## File Location `web/src/components/items/CreateItemPane.tsx` Supporting files: | File | Purpose | |------|---------| | `web/src/components/items/CategoryPicker.tsx` | Multi-stage domain/subcategory selector | | `web/src/components/items/FileDropZone.tsx` | Drag-and-drop file upload with MinIO presigned URLs | | `web/src/components/items/TagInput.tsx` | Multi-select tag input for projects | | `web/src/hooks/useFormDescriptor.ts` | Fetches and caches form descriptor from `/api/schemas/{name}/form` | | `web/src/hooks/useFileUpload.ts` | Manages presigned URL upload flow | ## Component Breakdown ### CreateItemPane Top-level orchestrator. Renders dynamic form from the form descriptor. **Props** (unchanged interface): ```typescript interface CreateItemPaneProps { onCreated: (item: Item) => void; onCancel: () => void; } ``` **State**: ```typescript const { descriptor, categories, loading } = useFormDescriptor(); const [category, setCategory] = useState(''); // selected category code, e.g. "F01" const [fields, setFields] = useState>({}); // all field values keyed by name const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); ``` A single `fields` record holds all form values (both item-level and property fields). The `ITEM_LEVEL_FIELDS` set (`description`, `item_type`, `sourcing_type`, `long_description`) determines which fields go into the top-level request vs. the `properties` map on submission. **Auto-derivation**: When a category is selected, `item_type` is automatically set based on the `derived_from_category` mapping in the form descriptor (e.g., category prefix `A` → `assembly`, `T` → `tooling`, default → `part`). **Dynamic rendering**: A `renderField()` function maps each field's `widget` type to the appropriate input: | Widget | Rendered As | |--------|-------------| | `text` | `` | | `number` | `` | | `textarea` | `