main #36

Merged
forbes merged 13 commits from main into production 2026-02-08 21:25:02 +00:00
7 changed files with 367 additions and 178 deletions

View File

@@ -13,7 +13,7 @@ Kindred Silo is an R&D-oriented item database with:
- **Role-based access control** (admin > editor > viewer) with API tokens and sessions
- **ODS import/export** for items, BOMs, and project sheets
- **Audit/completeness scoring** with weighted per-category property validation
- **Web UI** with htmx-based item browser, project management, and schema editing
- **Web UI** — React SPA (Vite + TypeScript, Catppuccin Mocha theme) for item browsing, project management, schema editing, and audit
- **CAD integration** via REST API ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc))
- **Physical inventory** tracking with hierarchical locations (schema ready)
@@ -22,24 +22,33 @@ Kindred Silo is an R&D-oriented item database with:
```
silo/
├── cmd/
│ ├── silo/ # CLI tool
│ └── silod/ # API server
│ ├── silo/ # CLI tool
│ └── silod/ # API server
├── internal/
│ ├── api/ # HTTP handlers, routes, templates (76 endpoints)
│ ├── auth/ # Authentication (local, LDAP, OIDC)
│ ├── config/ # Configuration loading
│ ├── db/ # PostgreSQL repositories
│ ├── migration/ # Property migration utilities
│ ├── odoo/ # Odoo ERP integration
│ ├── ods/ # ODS spreadsheet library
│ ├── partnum/ # Part number generation
│ ├── schema/ # YAML schema parsing
── storage/ # MinIO file storage
├── migrations/ # Database migrations (10 files)
├── schemas/ # Part numbering schemas (YAML)
├── deployments/ # Docker Compose and systemd configs
├── scripts/ # Deployment and setup scripts
└── docs/ # Documentation
│ ├── api/ # HTTP handlers and routes (75 endpoints)
│ ├── auth/ # Authentication (local, LDAP, OIDC)
│ ├── config/ # Configuration loading
│ ├── db/ # PostgreSQL repositories
│ ├── migration/ # Property migration utilities
│ ├── odoo/ # Odoo ERP integration
│ ├── ods/ # ODS spreadsheet library
│ ├── partnum/ # Part number generation
│ ├── schema/ # YAML schema parsing
── storage/ # MinIO file storage
│ └── testutil/ # Test helpers
├── web/ # React SPA (Vite + TypeScript)
│ └── src/
│ ├── api/ # API client and type definitions
│ ├── components/ # Reusable UI components
│ ├── context/ # Auth context provider
│ ├── hooks/ # Custom React hooks
│ ├── pages/ # Page components (Items, Projects, Schemas, Settings, Audit, Login)
│ └── styles/ # Catppuccin Mocha theme and global styles
├── migrations/ # Database migrations (11 files)
├── schemas/ # Part numbering schemas (YAML)
├── deployments/ # Docker Compose and systemd configs
├── scripts/ # Deployment and setup scripts
└── docs/ # Documentation
```
## Quick Start
@@ -95,12 +104,16 @@ The server provides the REST API and ODS endpoints consumed by these clients.
| Document | Description |
|----------|-------------|
| [docs/AUTH.md](docs/AUTH.md) | Authentication system design |
| [docs/AUTH_USER_GUIDE.md](docs/AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide |
| [docs/SPECIFICATION.md](docs/SPECIFICATION.md) | Full design specification and API reference |
| [docs/STATUS.md](docs/STATUS.md) | Implementation status |
| [ROADMAP.md](ROADMAP.md) | Feature roadmap and gap analysis |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide |
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Configuration reference (all `config.yaml` options) |
| [docs/AUTH.md](docs/AUTH.md) | Authentication system design |
| [docs/AUTH_USER_GUIDE.md](docs/AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles |
| [docs/GAP_ANALYSIS.md](docs/GAP_ANALYSIS.md) | Gap analysis and revision control roadmap |
| [docs/COMPONENT_AUDIT.md](docs/COMPONENT_AUDIT.md) | Component audit tool design |
| [ROADMAP.md](ROADMAP.md) | Feature roadmap and SOLIDWORKS PDM comparison |
| [frontend-spec.md](frontend-spec.md) | React SPA frontend specification |
## License

View File

@@ -39,7 +39,7 @@ This document compares Silo's current capabilities against SOLIDWORKS PDM—the
### Implemented Features (MVP Complete)
#### Core Database System
- PostgreSQL schema with 10 migrations
- PostgreSQL schema with 11 migrations
- UUID-based identifiers throughout
- Soft delete support via `archived_at` timestamps
- Atomic sequence generation for part numbers
@@ -92,14 +92,14 @@ This document compares Silo's current capabilities against SOLIDWORKS PDM—the
- Template generation for import formatting
#### API & Web Interface
- REST API with 74 endpoints
- REST API with 75 endpoints
- Authentication: local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak
- Role-based access control (admin > editor > viewer)
- API token management (SHA-256 hashed)
- Session management (PostgreSQL-backed, 24h lifetime)
- CSRF protection (nosurf on web forms)
- Middleware: logging, CORS, recovery, request ID
- Web UI for items, projects, schemas, audit (htmx)
- Web UI — React SPA (Vite + TypeScript, Catppuccin Mocha theme)
- Fuzzy search
- Health and readiness probes
@@ -123,7 +123,7 @@ This document compares Silo's current capabilities against SOLIDWORKS PDM—the
| Part number validation | Not started | API accepts but doesn't validate format |
| Location hierarchy CRUD | Schema only | Tables exist, no API endpoints |
| Inventory tracking | Schema only | Tables exist, no API endpoints |
| Unit tests | Minimal | 1 Go test file (`internal/ods/ods_test.go`) |
| Unit tests | Partial | 9 Go test files across api, db, ods, partnum, schema packages |
### Infrastructure Status
@@ -255,14 +255,14 @@ CAD integration is maintained in separate repositories ([silo-mod](https://git.k
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| ERP integration | SAP, Dynamics, etc. | Partial (Odoo stubs) | Medium | Complex |
| API access | Full COM/REST API | Full REST API (74 endpoints) | - | - |
| API access | Full COM/REST API | Full REST API (75 endpoints) | - | - |
| Dispatch scripts | Automation without coding | None | Medium | Moderate |
| Task scheduler | Background processing | None | Medium | Moderate |
| Email system | SMTP integration | None | High | Simple |
| Web portal | Browser access | Full (htmx + auth) | - | - |
| Web portal | Browser access | Full (React SPA + auth) | - | - |
**Gap Analysis:**
Silo has a comprehensive REST API (74 endpoints) and a full web UI with authentication. Odoo ERP integration has config/sync-log scaffolding but push/pull operations are stubs. Remaining gaps: email notifications, task scheduler, dispatch automation.
Silo has a comprehensive REST API (75 endpoints) and a full web UI with authentication. Odoo ERP integration has config/sync-log scaffolding but push/pull operations are stubs. Remaining gaps: email notifications, task scheduler, dispatch automation.
---
@@ -528,9 +528,9 @@ File storage works well. Thumbnail generation and file preview would significant
| **Data** | CSV import/export | Yes | Yes | Yes | - |
| | ODS import/export | No | No | Yes | - |
| | Project management | Yes | Yes | Yes | - |
| **Integration** | API | Limited | Full | Full REST (74) | - |
| **Integration** | API | Limited | Full | Full REST (75) | - |
| | ERP connectors | No | Yes | Partial (Odoo stubs) | Phase 6 |
| | Web access | No | Yes | Yes (htmx + auth) | - |
| | Web access | No | Yes | Yes (React SPA + auth) | - |
| **Files** | Versioning | Yes | Yes | Yes | - |
| | Preview | Yes | Yes | No | Phase 6 |
| | Multi-site | No | Yes | No | Not Planned |

View File

@@ -1,6 +1,6 @@
# Silo Gap Analysis and Revision Control Roadmap
**Date:** 2026-02-06
**Date:** 2026-02-08
**Status:** Analysis Complete (Updated)
---
@@ -21,9 +21,12 @@ This document analyzes the current state of the Silo project against its specifi
| `docs/SPECIFICATION.md` | Design specification, API reference | Current |
| `docs/STATUS.md` | Implementation status summary | Current |
| `docs/DEPLOYMENT.md` | Production deployment guide | Current |
| `docs/GAP_ANALYSIS.md` | SOLIDWORKS PDM comparison, roadmap | Current |
| `ROADMAP.md` | Feature roadmap and phases | Current |
| `silo-spec.md` | Redirect to `docs/SPECIFICATION.md` | Consolidated |
| `docs/CONFIGURATION.md` | Configuration reference (all config.yaml options) | Current |
| `docs/AUTH.md` | Authentication system design | Current |
| `docs/AUTH_USER_GUIDE.md` | User guide for login, tokens, and roles | Current |
| `docs/GAP_ANALYSIS.md` | Revision control roadmap | Current |
| `ROADMAP.md` | Feature roadmap and SOLIDWORKS PDM comparison | Current |
| `frontend-spec.md` | React SPA frontend specification | Current |
### 1.2 Documentation Gaps (Priority Order)
@@ -34,7 +37,7 @@ This document analyzes the current state of the Silo project against its specifi
| **API Reference** | Users cannot integrate programmatically | Medium | Addressed in SPECIFICATION.md Section 11 |
| **Deployment Guide** | Cannot deploy to production | Medium | Complete (`docs/DEPLOYMENT.md`) |
| **Database Schema Guide** | Migration troubleshooting difficult | Low | Open |
| **Configuration Reference** | config.yaml options undocumented | Low | Open |
| **Configuration Reference** | config.yaml options undocumented | Low | Complete (`docs/CONFIGURATION.md`) |
#### Medium Priority
@@ -54,10 +57,10 @@ This document analyzes the current state of the Silo project against its specifi
### 1.3 Recommended Actions
1. ~~**Consolidate specs**: Remove `silo-spec.md` duplicate~~ Done
1. ~~**Consolidate specs**: Remove `silo-spec.md` duplicate~~ Done (deleted)
2. ~~**Create API reference**: Full REST endpoint documentation~~ Addressed in SPECIFICATION.md
3. ~~**Create `docs/DEPLOYMENT.md`**: Production deployment guide~~ Done
4. **Create configuration reference**: Document all `config.yaml` options
4. ~~**Create configuration reference**: Document all `config.yaml` options~~ Done (`docs/CONFIGURATION.md`)
5. **Create database schema guide**: Document migrations and troubleshooting
---
@@ -207,7 +210,7 @@ Audit logging is implemented via migration 009 with the `audit_log` table and co
- `GET /api/audit/completeness` — summary of all items
- `GET /api/audit/completeness/{partNumber}` — per-item scoring with weighted fields and tier classification
Code: `internal/api/handlers_audit.go`
Code: `internal/api/audit_handlers.go`
---
@@ -304,7 +307,6 @@ Effort: Medium | Priority: Low | Risk: Low
7. **Release Management** - Product milestone tracking
8. **Thumbnail Generation** - Visual preview capability
9. **Documentation Overhaul** - API reference, deployment guide
### Long-term (Future)
@@ -351,19 +353,33 @@ These design decisions remain unresolved:
## Appendix A: File Structure
Revision endpoints, status, labels, authentication, and audit logging are implemented. Current structure:
Revision endpoints, status, labels, authentication, audit logging, and file attachments are implemented. Current structure:
```
internal/
api/
handlers_audit.go # Audit/completeness endpoints (implemented)
middleware.go # Auth middleware (implemented)
audit_handlers.go # Audit/completeness endpoints
auth_handlers.go # Login, tokens, OIDC
bom_handlers.go # Flat BOM, cost roll-up
file_handlers.go # Presigned uploads, item files, thumbnails
handlers.go # Items, schemas, projects, revisions
middleware.go # Auth middleware
odoo_handlers.go # Odoo integration endpoints
routes.go # Route registration (75 endpoints)
search.go # Fuzzy search
auth/
auth.go # Auth service: local, LDAP, OIDC (implemented)
auth.go # Auth service: local, LDAP, OIDC
db/
items.go # Item and revision repository
item_files.go # File attachment repository
relationships.go # BOM repository
projects.go # Project repository
storage/
storage.go # MinIO file storage helpers
migrations/
008_odoo_integration.sql # Odoo ERP tables (implemented)
009_auth.sql # Auth + audit tables (implemented)
010_item_extended_fields.sql # Extended item fields (implemented)
001_initial.sql # Core schema
...
011_item_files.sql # Item file attachments (latest)
```
Future features would add:
@@ -371,13 +387,13 @@ Future features would add:
```
internal/
api/
handlers_lock.go # Locking endpoints
lock_handlers.go # Locking endpoints
db/
locks.go # Lock repository
releases.go # Release repository
migrations/
011_item_locks.sql # Locking table
012_releases.sql # Release management
012_item_locks.sql # Locking table
013_releases.sql # Release management
```
---
@@ -407,6 +423,18 @@ GET /api/audit/completeness # All items completeness summary
GET /api/audit/completeness/{partNumber} # Per-item scoring
```
**File Attachments (Implemented):**
```
GET /api/auth/config # Auth config (public)
POST /api/uploads/presign # Presigned upload URL
GET /api/items/{pn}/files # List item files
POST /api/items/{pn}/files # Associate file with item
DELETE /api/items/{pn}/files/{fileId} # Delete file association
PUT /api/items/{pn}/thumbnail # Set item thumbnail
GET /api/items/{pn}/bom/flat # Flattened BOM
GET /api/items/{pn}/bom/cost # Assembly cost roll-up
```
**Locking (Not Implemented):**
```
POST /api/items/{pn}/lock # Acquire lock

View File

@@ -8,7 +8,7 @@
## 1. Overview
Silo is an item database with configurable part number generation, designed for R&D-oriented workflows. It provides revision tracking, BOM management, file versioning, and physical inventory location management through a REST API and web UI. CAD integration (FreeCAD workbench, LibreOffice Calc extension) is maintained in separate repositories ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)).
Silo is an item database with configurable part number generation, designed for R&D-oriented workflows. It provides revision tracking, BOM management, file versioning, and physical inventory location management through a REST API and web UI. CAD and workflow integration (FreeCAD workbench, LibreOffice Calc extension) is maintained in separate repositories ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)).
### 1.1 Core Philosophy
@@ -37,13 +37,13 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
┌─────────────────────────────────────────────────────────────┐
│ Silo Server (silod) │
│ - REST API (76 endpoints) │
│ - REST API (75 endpoints) │
│ - Authentication (local, LDAP, OIDC) │
│ - Schema parsing and validation │
│ - Part number generation engine │
│ - Revision management │
│ - Relationship graph / BOM │
│ - Web UI (htmx)
│ - Web UI (React SPA)
└─────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
@@ -68,7 +68,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
| CLI & API Server | Go (1.24) | chi/v5 router, pgx/v5 driver, zerolog |
| Authentication | Multi-backend | Local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak |
| Sessions | PostgreSQL pgxstore | alexedwards/scs, 24h lifetime |
| Web UI | Go html/template + htmx | Lightweight, minimal JS |
| Web UI | React 19, Vite 6, TypeScript 5.7 | Catppuccin Mocha theme, inline styles |
---
@@ -362,15 +362,35 @@ Recommendation: Pessimistic locking for CAD files (merge is impractical).
## 6. Web Interface
### 6.1 Features
### 6.1 Architecture
- **Browse**: Navigate item hierarchy (project → assembly → subassembly → part)
- **Search**: Full-text search across part numbers, descriptions, properties
- **View**: Item details, revision history, relationships, location
- **BOM Viewer**: Expandable tree view of assembly structure
- **"Open in FreeCAD"**: Launch FreeCAD with specific item via URI handler
The web UI is a React single-page application served at `/` by the Go server. The SPA communicates with the backend exclusively via the JSON REST API at `/api/*`.
### 6.2 URI Handler
- **Stack**: React 19, React Router 7, Vite 6, TypeScript 5.7
- **Theme**: Catppuccin Mocha (dark) via CSS custom properties
- **Styling**: Inline `React.CSSProperties` — no CSS modules, no Tailwind
- **State**: Local `useState` + custom hooks (no Redux/Zustand)
- **SPA serving**: `web/dist/` served by Go's `NotFound` handler with `index.html` fallback for client-side routing
### 6.2 Pages
| Page | Route | Description |
|------|-------|-------------|
| Items | `/` | Master-detail layout with resizable split panel, sortable table, 5-tab detail view (Main, Properties, Revisions, BOM, Where Used), in-pane CRUD forms |
| Projects | `/projects` | Project CRUD with sortable table, in-pane forms |
| Schemas | `/schemas` | Schema browser with collapsible segments, enum value CRUD |
| Settings | `/settings` | Account info, API token management |
| Audit | `/audit` | Item completeness scoring |
| Login | `/login` | Username/password form, conditional OIDC button |
### 6.3 Design Patterns
- **In-pane forms**: Create/Edit/Delete forms render in the detail pane area (Infor ERP-style), not as modal overlays
- **Permission checks**: Write actions conditionally rendered based on user role
- **Fuzzy search**: Debounced search with scope toggle (All/PN/Description)
- **Persistence**: `useLocalStorage` hook for user preferences (layout mode, column visibility)
### 6.4 URI Handler
Register `silo://` protocol handler:
@@ -379,12 +399,7 @@ silo://open/PROTO-AS-0001 # Open latest revision
silo://open/PROTO-AS-0001?rev=3 # Open specific revision
```
### 6.3 Technology
- **Backend**: Go with standard library HTTP
- **Frontend**: htmx for interactivity, minimal JavaScript
- **Templates**: Go html/template
- **Search**: PostgreSQL full-text search (pg_trgm for fuzzy matching)
See [frontend-spec.md](../frontend-spec.md) for full component specifications.
---
@@ -583,7 +598,7 @@ See [AUTH.md](AUTH.md) for full architecture details and [AUTH_USER_GUIDE.md](AU
## 11. API Design
### 11.1 REST Endpoints (Implemented)
### 11.1 REST Endpoints (75 Implemented)
```
# Health (no auth)
@@ -597,21 +612,18 @@ POST /logout # Logout
GET /auth/oidc # OIDC login redirect
GET /auth/callback # OIDC callback
# Web UI (auth + CSRF)
GET / # Items page
GET /projects # Projects page
GET /schemas # Schemas page
GET /audit # Audit/completeness page
GET /settings # User settings / token management
POST /settings/tokens # Create API token (web)
POST /settings/tokens/{id}/revoke # Revoke API token (web)
# Public API (no auth required)
GET /api/auth/config # Auth backend configuration (for login UI)
# Auth API
# Auth API (require auth)
GET /api/auth/me # Current authenticated user
GET /api/auth/tokens # List user's API tokens
POST /api/auth/tokens # Create API token
DELETE /api/auth/tokens/{id} # Revoke API token
# Presigned Uploads (editor)
POST /api/uploads/presign # Get presigned MinIO upload URL [editor]
# Schemas (read: viewer, write: editor)
GET /api/schemas # List all schemas
GET /api/schemas/{name} # Get schema details
@@ -659,9 +671,13 @@ PATCH /api/items/{partNumber}/revisions/{revision} # Update status/labe
POST /api/items/{partNumber}/revisions/{revision}/rollback # Rollback to revision [editor]
# Files
GET /api/items/{partNumber}/files # List item file attachments
GET /api/items/{partNumber}/file # Download latest file
GET /api/items/{partNumber}/file/{revision} # Download file at revision
POST /api/items/{partNumber}/file # Upload file [editor]
POST /api/items/{partNumber}/files # Associate uploaded file with item [editor]
DELETE /api/items/{partNumber}/files/{fileId} # Remove file association [editor]
PUT /api/items/{partNumber}/thumbnail # Set item thumbnail [editor]
# BOM
GET /api/items/{partNumber}/bom # List direct children
@@ -718,11 +734,11 @@ POST /api/inventory/{partNumber}/move
### 12.1 Implemented
- [x] PostgreSQL database schema (10 migrations)
- [x] PostgreSQL database schema (11 migrations)
- [x] YAML schema parser for part numbering
- [x] Part number generation engine
- [x] CLI tool (`cmd/silo`)
- [x] API server (`cmd/silod`) with 76 endpoints
- [x] API server (`cmd/silod`) with 75 endpoints
- [x] MinIO integration for file storage with versioning
- [x] BOM relationships (component, alternate, reference)
- [x] Multi-level BOM (recursive expansion with configurable depth)
@@ -736,7 +752,8 @@ POST /api/inventory/{partNumber}/move
- [x] Project management with many-to-many item tagging
- [x] CSV import/export with dry-run validation
- [x] ODS spreadsheet import/export (items, BOMs, project sheets)
- [x] Web UI for items, projects, schemas, audit (htmx)
- [x] Web UI for items, projects, schemas, audit, settings (React SPA)
- [x] File attachments with presigned upload and thumbnail support
- [x] Authentication (local, LDAP, OIDC) with role-based access control
- [x] API token management (SHA-256 hashed)
- [x] Session management (PostgreSQL-backed)
@@ -757,7 +774,7 @@ POST /api/inventory/{partNumber}/move
### 12.3 Not Started
- [ ] Unit tests (Go server — minimal coverage exists)
- [ ] Unit tests (Go server — 9 test files exist, coverage is partial)
- [ ] Schema migration tooling
- [ ] Checkout locking
- [ ] Approval workflows

View File

@@ -1,6 +1,6 @@
# Silo Development Status
**Last Updated:** 2026-02-06
**Last Updated:** 2026-02-08
---
@@ -10,10 +10,10 @@
| Component | Status | Notes |
|-----------|--------|-------|
| PostgreSQL schema | Complete | 10 migrations applied |
| PostgreSQL schema | Complete | 11 migrations applied |
| YAML schema parser | Complete | Supports enum, serial, constant, string segments |
| Part number generator | Complete | Scoped sequences, category-based format |
| API server (`silod`) | Complete | 74 REST endpoints via chi/v5 |
| API server (`silod`) | Complete | 75 REST endpoints via chi/v5 |
| CLI tool (`silo`) | Complete | Item registration and management |
| MinIO file storage | Complete | Upload, download, versioning, checksums |
| Revision control | Complete | Append-only history, rollback, comparison, status/labels |
@@ -28,7 +28,8 @@
| Audit logging | Complete | audit_log table, completeness scoring |
| CSRF protection | Complete | nosurf on web forms |
| Fuzzy search | Complete | sahilm/fuzzy library |
| Web UI | Complete | Items, projects, schemas, audit pages (htmx) |
| Web UI | Complete | React SPA (Vite + TypeScript), 6 pages, Catppuccin Mocha theme |
| File attachments | Complete | Presigned uploads, item file association, thumbnails |
| Odoo ERP integration | Partial | Config and sync-log CRUD functional; push/pull are stubs |
| Docker Compose | Complete | Dev and production configurations |
| Deployment scripts | Complete | setup-host, deploy, init-db, setup-ipa-nginx |
@@ -46,7 +47,7 @@ FreeCAD workbench and LibreOffice Calc extension are maintained in separate repo
| Inventory API endpoints | Database tables exist, no REST handlers |
| Date segment type | Schema parser placeholder only |
| Part number format validation | API accepts but does not validate format on creation |
| Unit tests | Minimal: 1 Go test file (`internal/ods/ods_test.go`) |
| Unit tests | 9 Go test files across api, db, ods, partnum, schema packages |
---
@@ -92,3 +93,4 @@ The schema defines 170 category codes across 10 groups:
| 008_odoo_integration.sql | Odoo ERP integration tables (integrations, sync_log) |
| 009_auth.sql | Authentication system (users, api_tokens, sessions, audit_log, user tracking columns) |
| 010_item_extended_fields.sql | Extended item fields (sourcing_type, sourcing_link, standard_cost, long_description) |
| 011_item_files.sql | Item file attachments (item_files table, thumbnail_key column) |

View File

@@ -1,10 +1,10 @@
# Silo Frontend Specification
Current as of 2026-02-06. Tracks the React + Vite + TypeScript frontend migration (epic #6).
Current as of 2026-02-08. Documents the React + Vite + TypeScript frontend (migration from Go templates is complete).
## Overview
The Silo web UI is being migrated from server-rendered Go templates with vanilla JavaScript (~7,000 lines across 7 templates) to a React single-page application. The Go API server remains unchanged — it serves JSON at `/api/*` and the React app consumes it.
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
@@ -19,22 +19,21 @@ The Silo web UI is being migrated from server-rendered Go templates with vanilla
| 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 | Not started |
| 4 | #10 | Remove Go templates, Docker integration, cleanup | Complete |
## Architecture
```
Browser
└── React SPA (served at /app/* during transition, / after Phase 4)
└── 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 (unchanged)
├── /api/* JSON REST API
├── /login, /logout Session auth endpoints (form POST)
├── /auth/oidc OIDC redirect flow
── /app/* React SPA static files (current transition)
└── /* Go template pages (removed in Phase 4)
── /* React SPA (NotFound handler serves index.html for client-side routing)
```
### Auth Flow
@@ -101,7 +100,7 @@ web/
└── AuditPage.tsx Audit completeness (placeholder, expanded in Issue #5)
```
**Total**: 32 source files, ~5,300 lines of TypeScript/TSX.
**Total**: ~40 source files, ~7,600 lines of TypeScript/TSX.
## Design System
@@ -229,25 +228,17 @@ Basic table showing audit completeness data from `GET /api/audit/completeness`.
**Import**: CSVImportResult, CSVImportError
**Errors**: ErrorResponse
## Remaining Work
## Completed Work
### Issue #10: Remove Go Templates + Docker Integration
### Issue #10: Remove Go Templates + Docker Integration -- COMPLETE
The final phase completes the migration:
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.
**Remove Go templates**: Delete `internal/api/templates/` (7 HTML files), remove template loading code, remove web handler route group and CSRF middleware for web routes, remove `HandleIndex`, `HandleProjects`, `HandleSchemas`, `HandleSettings` handlers.
**SPA serving**: Serve `web/dist/` at `/` with SPA fallback (non-API routes return `index.html`). Either `go:embed` for single-binary deployment or filesystem serving. `/api/*` routes must take precedence. Cache headers for Vite's hashed assets.
**Docker**: Update `build/package/Dockerfile` to multi-stage — Stage 1: Node (`npm ci && npm run build`), Stage 2: Go build with `web/dist/`, Final: minimal Alpine image with single binary.
**Makefile**: Existing `web-install`, `web-dev`, `web-build` targets need verification. `make build` should include the web build step. `make clean` should include `web/dist/`.
**Acceptance criteria**: Single Docker image serves both API and React frontend. `make build` produces working binary. No Go template code remains. All pages accessible at React Router paths.
### Remaining Work
### Issue #5: Component Audit UI (future)
After migration completes, 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.
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
@@ -572,11 +563,11 @@ The right sidebar is divided into three sections with `borderBottom: 1px solid v
**Thumbnail**: A 4:3 aspect ratio placeholder box (`--ctp-crust` bg, `--ctp-surface0` border) with centered text "Generated from CAD file or upload manually". Clicking opens file picker filtered to images. If a thumbnail is uploaded, show it as an `<img>` with `object-fit: cover`.
## Backend Changes Required
## Backend Changes
The following API additions are needed. These should be tracked as sub-tasks or a separate issue.
Items 1-3 and 5 below are implemented (migration `011_item_files.sql`, `internal/api/file_handlers.go`). Item 4 (hierarchical categories) remains open.
### 1. Presigned Upload URL
### 1. Presigned Upload URL -- IMPLEMENTED
```
POST /api/uploads/presign
@@ -586,7 +577,7 @@ Response: { "object_key": "uploads/tmp/{uuid}/{filename}", "upload_url": "https:
The Go handler generates a presigned PUT URL via the MinIO SDK. Objects are uploaded to a temporary prefix. On item creation, they're moved/linked to the item's permanent prefix.
### 2. File Association
### 2. File Association -- IMPLEMENTED
```
POST /api/items/{id}/files
@@ -596,7 +587,7 @@ Response: { "file_id": "uuid", "filename": "...", "size": ..., "created_at": "..
Moves the object from the temp prefix to `items/{item_id}/files/{file_id}` and creates a row in a new `item_files` table.
### 3. Thumbnail
### 3. Thumbnail -- IMPLEMENTED
```
PUT /api/items/{id}/thumbnail
@@ -606,7 +597,7 @@ Response: 204
Stores the thumbnail at `items/{item_id}/thumbnail.png` in MinIO. Updates `item.thumbnail_key` column.
### 4. Hierarchical Categories
### 4. Hierarchical Categories -- NOT IMPLEMENTED
If schemas don't currently support a hierarchical category tree, one of these approaches:
@@ -634,7 +625,7 @@ DELETE /api/categories/{id} → cascade check
**Recommendation**: Option B is more flexible and keeps categories as a first-class entity. The three-tier picker doesn't need to be limited to exactly three levels — it can render as many columns as the deepest category path, but three is the practical default (Domain → Group → Subtype).
### 5. Database Schema Addition
### 5. Database Schema Addition -- IMPLEMENTED
```sql
CREATE TABLE item_files (

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { get, post, del } from '../../api/client';
import type { Item, Project, Revision } from '../../api/types';
import { useState, useEffect } from "react";
import { get, post, del } from "../../api/client";
import type { Item, Project, Revision } from "../../api/types";
interface MainTabProps {
item: Item;
@@ -9,8 +9,14 @@ interface MainTabProps {
}
function formatDate(s: string) {
if (!s) return '—';
return new Date(s).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
if (!s) return "—";
return new Date(s).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function formatFileSize(bytes: number) {
@@ -21,19 +27,23 @@ function formatFileSize(bytes: number) {
}
export function MainTab({ item, onReload, isEditor }: MainTabProps) {
const [itemProjects, setItemProjects] = useState<string[]>([]);
const [itemProjects, setItemProjects] = useState<Project[]>([]);
const [allProjects, setAllProjects] = useState<Project[]>([]);
const [latestRev, setLatestRev] = useState<Revision | null>(null);
const [addProject, setAddProject] = useState('');
const [addProject, setAddProject] = useState("");
useEffect(() => {
get<string[]>(`/api/items/${encodeURIComponent(item.part_number)}/projects`)
get<Project[]>(
`/api/items/${encodeURIComponent(item.part_number)}/projects`,
)
.then(setItemProjects)
.catch(() => setItemProjects([]));
get<Project[]>('/api/projects')
get<Project[]>("/api/projects")
.then(setAllProjects)
.catch(() => {});
get<Revision[]>(`/api/items/${encodeURIComponent(item.part_number)}/revisions`)
get<Revision[]>(
`/api/items/${encodeURIComponent(item.part_number)}/revisions`,
)
.then((revs) => {
if (revs.length > 0) setLatestRev(revs[revs.length - 1]!);
})
@@ -43,67 +53,144 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
const handleAddProject = async () => {
if (!addProject) return;
try {
await post(`/api/items/${encodeURIComponent(item.part_number)}/projects`, { projects: [addProject] });
setItemProjects((prev) => [...prev, addProject]);
setAddProject('');
await post(
`/api/items/${encodeURIComponent(item.part_number)}/projects`,
{ projects: [addProject] },
);
const proj = allProjects.find((p) => p.code === addProject);
if (proj) setItemProjects((prev) => [...prev, proj]);
setAddProject("");
onReload();
} catch (e) {
alert(e instanceof Error ? e.message : 'Failed to add project');
alert(e instanceof Error ? e.message : "Failed to add project");
}
};
const handleRemoveProject = async (code: string) => {
try {
await del(`/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`);
setItemProjects((prev) => prev.filter((p) => p !== code));
await del(
`/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`,
);
setItemProjects((prev) => prev.filter((p) => p.code !== code));
onReload();
} catch (e) {
alert(e instanceof Error ? e.message : 'Failed to remove project');
alert(e instanceof Error ? e.message : "Failed to remove project");
}
};
const row = (label: string, value: React.ReactNode) => (
<div style={{ display: 'flex', gap: '1rem', padding: '0.3rem 0', fontSize: '0.85rem' }}>
<span style={{ width: 120, flexShrink: 0, color: 'var(--ctp-subtext0)' }}>{label}</span>
<span style={{ color: 'var(--ctp-text)' }}>{value}</span>
<div
style={{
display: "flex",
gap: "1rem",
padding: "0.3rem 0",
fontSize: "0.85rem",
}}
>
<span style={{ width: 120, flexShrink: 0, color: "var(--ctp-subtext0)" }}>
{label}
</span>
<span style={{ color: "var(--ctp-text)" }}>{value}</span>
</div>
);
return (
<div>
{row('Part Number', <span style={{ fontFamily: "'JetBrains Mono', monospace", color: 'var(--ctp-peach)' }}>{item.part_number}</span>)}
{row('Description', item.description)}
{row('Type', item.item_type)}
{row('Sourcing', item.sourcing_type || '—')}
{item.sourcing_link && row('Source Link', <a href={item.sourcing_link} target="_blank" rel="noreferrer">{item.sourcing_link}</a>)}
{item.standard_cost != null && row('Std Cost', `$${item.standard_cost.toFixed(2)}`)}
{row('Revision', `Rev ${item.current_revision}`)}
{row('Created', formatDate(item.created_at))}
{row('Updated', formatDate(item.updated_at))}
{row(
"Part Number",
<span
style={{
fontFamily: "'JetBrains Mono', monospace",
color: "var(--ctp-peach)",
}}
>
{item.part_number}
</span>,
)}
{row("Description", item.description)}
{row("Type", item.item_type)}
{row("Sourcing", item.sourcing_type || "—")}
{item.sourcing_link &&
row(
"Source Link",
<a href={item.sourcing_link} target="_blank" rel="noreferrer">
{item.sourcing_link}
</a>,
)}
{item.standard_cost != null &&
row("Std Cost", `$${item.standard_cost.toFixed(2)}`)}
{row("Revision", `Rev ${item.current_revision}`)}
{row("Created", formatDate(item.created_at))}
{row("Updated", formatDate(item.updated_at))}
{item.long_description && (
<div style={{ marginTop: '0.75rem', padding: '0.5rem', backgroundColor: 'var(--ctp-surface0)', borderRadius: '0.4rem', fontSize: '0.85rem' }}>
<div style={{ color: 'var(--ctp-subtext0)', fontSize: '0.75rem', marginBottom: '0.25rem' }}>Long Description</div>
<div style={{ whiteSpace: 'pre-wrap' }}>{item.long_description}</div>
<div
style={{
marginTop: "0.75rem",
padding: "0.5rem",
backgroundColor: "var(--ctp-surface0)",
borderRadius: "0.4rem",
fontSize: "0.85rem",
}}
>
<div
style={{
color: "var(--ctp-subtext0)",
fontSize: "0.75rem",
marginBottom: "0.25rem",
}}
>
Long Description
</div>
<div style={{ whiteSpace: "pre-wrap" }}>{item.long_description}</div>
</div>
)}
{/* Project Tags */}
<div style={{ marginTop: '0.75rem' }}>
<div style={{ color: 'var(--ctp-subtext0)', fontSize: '0.75rem', marginBottom: '0.25rem' }}>Projects</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', alignItems: 'center' }}>
{itemProjects.map((code) => (
<span key={code} style={{
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
padding: '0.1rem 0.5rem', borderRadius: '1rem',
backgroundColor: 'rgba(203,166,247,0.15)', color: 'var(--ctp-mauve)',
fontSize: '0.75rem',
}}>
{code}
<div style={{ marginTop: "0.75rem" }}>
<div
style={{
color: "var(--ctp-subtext0)",
fontSize: "0.75rem",
marginBottom: "0.25rem",
}}
>
Projects
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "0.25rem",
alignItems: "center",
}}
>
{itemProjects.map((proj) => (
<span
key={proj.code}
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.25rem",
padding: "0.1rem 0.5rem",
borderRadius: "1rem",
backgroundColor: "rgba(203,166,247,0.15)",
color: "var(--ctp-mauve)",
fontSize: "0.75rem",
}}
>
{proj.code}
{isEditor && (
<button
onClick={() => void handleRemoveProject(code)}
style={{ background: 'none', border: 'none', color: 'var(--ctp-overlay0)', cursor: 'pointer', fontSize: '0.8rem', padding: 0 }}
onClick={() => void handleRemoveProject(proj.code)}
style={{
background: "none",
border: "none",
color: "var(--ctp-overlay0)",
cursor: "pointer",
fontSize: "0.8rem",
padding: 0,
}}
>
×
</button>
@@ -116,22 +203,36 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
value={addProject}
onChange={(e) => setAddProject(e.target.value)}
style={{
padding: '0.1rem 0.3rem', fontSize: '0.75rem',
backgroundColor: 'var(--ctp-surface0)', border: '1px solid var(--ctp-surface1)',
borderRadius: '0.3rem', color: 'var(--ctp-text)',
padding: "0.1rem 0.3rem",
fontSize: "0.75rem",
backgroundColor: "var(--ctp-surface0)",
border: "1px solid var(--ctp-surface1)",
borderRadius: "0.3rem",
color: "var(--ctp-text)",
}}
>
<option value="">+</option>
{allProjects
.filter((p) => !itemProjects.includes(p.code))
.map((p) => <option key={p.code} value={p.code}>{p.code}</option>)}
.filter((p) => !itemProjects.some((ip) => ip.code === p.code))
.map((p) => (
<option key={p.code} value={p.code}>
{p.code}
</option>
))}
</select>
{addProject && (
<button onClick={() => void handleAddProject()} style={{
padding: '0.1rem 0.4rem', fontSize: '0.7rem', border: 'none',
backgroundColor: 'var(--ctp-mauve)', color: 'var(--ctp-crust)',
borderRadius: '0.3rem', cursor: 'pointer',
}}>
<button
onClick={() => void handleAddProject()}
style={{
padding: "0.1rem 0.4rem",
fontSize: "0.7rem",
border: "none",
backgroundColor: "var(--ctp-mauve)",
color: "var(--ctp-crust)",
borderRadius: "0.3rem",
cursor: "pointer",
}}
>
Add
</button>
)}
@@ -142,21 +243,58 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) {
{/* File Info */}
{latestRev?.file_key && (
<div style={{ marginTop: '0.75rem', padding: '0.5rem', backgroundColor: 'var(--ctp-surface0)', borderRadius: '0.4rem' }}>
<div style={{ color: 'var(--ctp-subtext0)', fontSize: '0.75rem', marginBottom: '0.25rem' }}>File Attachment (Rev {latestRev.revision_number})</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', fontSize: '0.85rem' }}>
{latestRev.file_size != null && <span>{formatFileSize(latestRev.file_size)}</span>}
<div
style={{
marginTop: "0.75rem",
padding: "0.5rem",
backgroundColor: "var(--ctp-surface0)",
borderRadius: "0.4rem",
}}
>
<div
style={{
color: "var(--ctp-subtext0)",
fontSize: "0.75rem",
marginBottom: "0.25rem",
}}
>
File Attachment (Rev {latestRev.revision_number})
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
fontSize: "0.85rem",
}}
>
{latestRev.file_size != null && (
<span>{formatFileSize(latestRev.file_size)}</span>
)}
{latestRev.file_checksum && (
<span title={latestRev.file_checksum} style={{ color: 'var(--ctp-overlay1)', fontFamily: 'monospace', fontSize: '0.75rem' }}>
<span
title={latestRev.file_checksum}
style={{
color: "var(--ctp-overlay1)",
fontFamily: "monospace",
fontSize: "0.75rem",
}}
>
SHA256: {latestRev.file_checksum.substring(0, 12)}...
</span>
)}
<button
onClick={() => { window.location.href = `/api/items/${encodeURIComponent(item.part_number)}/file/${latestRev.revision_number}`; }}
onClick={() => {
window.location.href = `/api/items/${encodeURIComponent(item.part_number)}/file/${latestRev.revision_number}`;
}}
style={{
padding: '0.2rem 0.5rem', fontSize: '0.8rem', border: 'none',
backgroundColor: 'var(--ctp-surface1)', color: 'var(--ctp-text)',
borderRadius: '0.3rem', cursor: 'pointer',
padding: "0.2rem 0.5rem",
fontSize: "0.8rem",
border: "none",
backgroundColor: "var(--ctp-surface1)",
color: "var(--ctp-text)",
borderRadius: "0.3rem",
cursor: "pointer",
}}
>
Download