Merge branch 'main' into feat/placeholder-icons-for-missing-freecad-icons
All checks were successful
Build and Test / build (pull_request) Successful in 39m19s

This commit is contained in:
2026-02-12 23:20:47 +00:00
16 changed files with 853 additions and 272 deletions

View File

@@ -38,7 +38,7 @@ API tokens allow the FreeCAD plugin, scripts, and CI pipelines to authenticate w
### Creating a Token (CLI)
```sh
export SILO_API_URL=https://silo.kindred.internal
export SILO_API_URL=https://silo.example.internal
export SILO_API_TOKEN=silo_<your-existing-token>
silo token create --name "CI pipeline"
@@ -140,7 +140,7 @@ auth:
ldap:
enabled: true
url: "ldaps://ipa.kindred.internal"
url: "ldaps://ipa.example.internal"
base_dn: "dc=kindred,dc=internal"
user_search_dn: "cn=users,cn=accounts,dc=kindred,dc=internal"
user_attr: "uid"
@@ -170,10 +170,10 @@ auth:
oidc:
enabled: true
issuer_url: "https://keycloak.kindred.internal/realms/silo"
issuer_url: "https://keycloak.example.internal/realms/silo"
client_id: "silo"
client_secret: "" # Set via SILO_OIDC_CLIENT_SECRET
redirect_url: "https://silo.kindred.internal/auth/callback"
redirect_url: "https://silo.example.internal/auth/callback"
scopes: ["openid", "profile", "email"]
admin_role: "silo-admin"
editor_role: "silo-editor"
@@ -186,7 +186,7 @@ auth:
auth:
cors:
allowed_origins:
- "https://silo.kindred.internal"
- "https://silo.example.internal"
```
## Environment Variables
@@ -254,4 +254,4 @@ UPDATE users SET password_hash = '<bcrypt-hash>', is_active = true WHERE usernam
- Verify the token is set in FreeCAD preferences or `SILO_API_TOKEN`
- Check the API URL points to the correct server
- Test with curl: `curl -H "Authorization: Bearer silo_..." https://silo.kindred.internal/api/items`
- Test with curl: `curl -H "Authorization: Bearer silo_..." https://silo.example.internal/api/items`

View File

@@ -1,5 +1,9 @@
# Silo Production Deployment Guide
> **First-time setup?** See the [Installation Guide](INSTALL.md) for step-by-step
> instructions. This document covers ongoing maintenance and operations for an
> existing deployment.
This guide covers deploying Silo to a dedicated VM using external PostgreSQL and MinIO services.
## Table of Contents
@@ -17,7 +21,7 @@ This guide covers deploying Silo to a dedicated VM using external PostgreSQL and
```
┌─────────────────────────────────────────────────────────────────┐
│ silo.kindred.internal │
│ silo.example.internal │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ silod │ │
│ │ (Silo API Server) │ │
@@ -27,7 +31,7 @@ This guide covers deploying Silo to a dedicated VM using external PostgreSQL and
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ psql.kindred.internal │ │ minio.kindred.internal │
│ psql.example.internal │ │ minio.example.internal │
│ PostgreSQL 16 │ │ MinIO S3 │
│ :5432 │ │ :9000 (API) │
│ │ │ :9001 (Console) │
@@ -40,8 +44,8 @@ The following external services are already configured:
| Service | Host | Database/Bucket | User |
|---------|------|-----------------|------|
| PostgreSQL | psql.kindred.internal:5432 | silo | silo |
| MinIO | minio.kindred.internal:9000 | silo-files | silouser |
| PostgreSQL | psql.example.internal:5432 | silo | silo |
| MinIO | minio.example.internal:9000 | silo-files | silouser |
Migrations have been applied to the database.
@@ -53,10 +57,10 @@ For a fresh VM, run these commands:
```bash
# 1. SSH to the target host
ssh root@silo.kindred.internal
ssh root@silo.example.internal
# 2. Download and run setup script
curl -fsSL https://gitea.kindred.internal/kindred/silo-0062/raw/branch/main/scripts/setup-host.sh | bash
curl -fsSL https://git.kindred-systems.com/kindred/silo/raw/branch/main/scripts/setup-host.sh | bash
# 3. Configure credentials
nano /etc/silo/silod.env
@@ -69,16 +73,16 @@ nano /etc/silo/silod.env
## Initial Setup
Run the setup script once on `silo.kindred.internal` to prepare the host:
Run the setup script once on `silo.example.internal` to prepare the host:
```bash
# Option 1: If you have the repo locally
scp scripts/setup-host.sh root@silo.kindred.internal:/tmp/
ssh root@silo.kindred.internal 'bash /tmp/setup-host.sh'
scp scripts/setup-host.sh root@silo.example.internal:/tmp/
ssh root@silo.example.internal 'bash /tmp/setup-host.sh'
# Option 2: Direct on the host
ssh root@silo.kindred.internal
curl -fsSL https://git.kindred.internal/kindred/silo/raw/branch/main/scripts/setup-host.sh -o /tmp/setup-host.sh
ssh root@silo.example.internal
curl -fsSL https://git.kindred-systems.com/kindred/silo/raw/branch/main/scripts/setup-host.sh -o /tmp/setup-host.sh
bash /tmp/setup-host.sh
```
@@ -100,10 +104,10 @@ sudo nano /etc/silo/silod.env
Fill in the values:
```bash
# Database credentials (psql.kindred.internal)
# Database credentials (psql.example.internal)
SILO_DB_PASSWORD=your-database-password
# MinIO credentials (minio.kindred.internal)
# MinIO credentials (minio.example.internal)
SILO_MINIO_ACCESS_KEY=silouser
SILO_MINIO_SECRET_KEY=your-minio-secret-key
```
@@ -114,10 +118,10 @@ Before deploying, verify connectivity to external services:
```bash
# Test PostgreSQL
psql -h psql.kindred.internal -U silo -d silo -c 'SELECT 1'
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
# Test MinIO
curl -I http://minio.kindred.internal:9000/minio/health/live
curl -I http://minio.example.internal:9000/minio/health/live
```
---
@@ -129,7 +133,7 @@ curl -I http://minio.kindred.internal:9000/minio/health/live
To deploy or update Silo, run the deploy script on the target host:
```bash
ssh root@silo.kindred.internal
ssh root@silo.example.internal
/opt/silo/src/scripts/deploy.sh
```
@@ -165,7 +169,7 @@ sudo /opt/silo/src/scripts/deploy.sh --status
You can override the git repository URL and branch:
```bash
export SILO_REPO_URL=https://git.kindred.internal/kindred/silo.git
export SILO_REPO_URL=https://git.kindred-systems.com/kindred/silo.git
export SILO_BRANCH=main
sudo -E /opt/silo/src/scripts/deploy.sh
```
@@ -247,7 +251,7 @@ curl http://localhost:8080/ready
To update to the latest version:
```bash
ssh root@silo.kindred.internal
ssh root@silo.example.internal
/opt/silo/src/scripts/deploy.sh
```
@@ -269,7 +273,7 @@ When new migrations are added, run them manually:
ls -la /opt/silo/src/migrations/
# Run a specific migration
psql -h psql.kindred.internal -U silo -d silo -f /opt/silo/src/migrations/008_new_feature.sql
psql -h psql.example.internal -U silo -d silo -f /opt/silo/src/migrations/008_new_feature.sql
```
---
@@ -303,13 +307,13 @@ psql -h psql.kindred.internal -U silo -d silo -f /opt/silo/src/migrations/008_ne
1. Test network connectivity:
```bash
nc -zv psql.kindred.internal 5432
nc -zv psql.example.internal 5432
```
2. Test credentials:
```bash
source /etc/silo/silod.env
PGPASSWORD=$SILO_DB_PASSWORD psql -h psql.kindred.internal -U silo -d silo -c 'SELECT 1'
PGPASSWORD=$SILO_DB_PASSWORD psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
```
3. Check `pg_hba.conf` on PostgreSQL server allows connections from this host.
@@ -318,12 +322,12 @@ psql -h psql.kindred.internal -U silo -d silo -f /opt/silo/src/migrations/008_ne
1. Test network connectivity:
```bash
nc -zv minio.kindred.internal 9000
nc -zv minio.example.internal 9000
```
2. Test with curl:
```bash
curl -I http://minio.kindred.internal:9000/minio/health/live
curl -I http://minio.example.internal:9000/minio/health/live
```
3. Check SSL settings in config match MinIO setup:
@@ -340,8 +344,8 @@ curl -v http://localhost:8080/health
curl -v http://localhost:8080/ready
# If ready fails but health passes, check external services
psql -h psql.kindred.internal -U silo -d silo -c 'SELECT 1'
curl http://minio.kindred.internal:9000/minio/health/live
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
curl http://minio.example.internal:9000/minio/health/live
```
### Build Fails
@@ -391,14 +395,14 @@ This script:
getcert list
```
2. The silo config is already updated to use `https://silo.kindred.internal` as base URL. Restart silo:
2. The silo config is already updated to use `https://silo.example.internal` as base URL. Restart silo:
```bash
sudo systemctl restart silod
```
3. Test the setup:
```bash
curl https://silo.kindred.internal/health
curl https://silo.example.internal/health
```
### Certificate Management
@@ -422,7 +426,7 @@ For clients to trust the Silo HTTPS certificate, they need the IPA CA:
```bash
# Download CA cert
curl -o /tmp/ipa-ca.crt https://ipa.kindred.internal/ipa/config/ca.crt
curl -o /tmp/ipa-ca.crt https://ipa.example.internal/ipa/config/ca.crt
# Ubuntu/Debian
sudo cp /tmp/ipa-ca.crt /usr/local/share/ca-certificates/ipa-ca.crt

View File

@@ -365,7 +365,7 @@ internal/
handlers.go # Items, schemas, projects, revisions
middleware.go # Auth middleware
odoo_handlers.go # Odoo integration endpoints
routes.go # Route registration (75 endpoints)
routes.go # Route registration (78 endpoints)
search.go # Fuzzy search
auth/
auth.go # Auth service: local, LDAP, OIDC

View File

@@ -0,0 +1,518 @@
# Installing Silo
This guide covers two installation methods:
- **[Option A: Docker Compose](#option-a-docker-compose)** — self-contained stack with all services. Recommended for evaluation, small teams, and environments where Docker is the standard.
- **[Option B: Daemon Install](#option-b-daemon-install-systemd--external-services)** — systemd service with external PostgreSQL, MinIO, and optional LDAP/nginx. Recommended for production deployments integrated with existing infrastructure.
Both methods produce the same result: a running Silo server with a web UI, REST API, and authentication.
---
## Table of Contents
- [Prerequisites](#prerequisites)
- [Option A: Docker Compose](#option-a-docker-compose)
- [A.1 Prerequisites](#a1-prerequisites)
- [A.2 Clone the Repository](#a2-clone-the-repository)
- [A.3 Run the Setup Script](#a3-run-the-setup-script)
- [A.4 Start the Stack](#a4-start-the-stack)
- [A.5 Verify the Installation](#a5-verify-the-installation)
- [A.6 LDAP Users and Groups](#a6-ldap-users-and-groups)
- [A.7 Optional: Enable Nginx Reverse Proxy](#a7-optional-enable-nginx-reverse-proxy)
- [A.8 Stopping, Starting, and Upgrading](#a8-stopping-starting-and-upgrading)
- [Option B: Daemon Install (systemd + External Services)](#option-b-daemon-install-systemd--external-services)
- [B.1 Architecture Overview](#b1-architecture-overview)
- [B.2 Prerequisites](#b2-prerequisites)
- [B.3 Set Up External Services](#b3-set-up-external-services)
- [B.4 Prepare the Host](#b4-prepare-the-host)
- [B.5 Configure Credentials](#b5-configure-credentials)
- [B.6 Deploy](#b6-deploy)
- [B.7 Set Up Nginx and TLS](#b7-set-up-nginx-and-tls)
- [B.8 Verify the Installation](#b8-verify-the-installation)
- [B.9 Upgrading](#b9-upgrading)
- [Post-Install Configuration](#post-install-configuration)
- [Further Reading](#further-reading)
---
## Prerequisites
Regardless of which method you choose:
- **Git** to clone the repository
- A machine with at least **2 GB RAM** and **10 GB free disk**
- Network access to pull container images or download Go/Node toolchains
---
## Option A: Docker Compose
A single Docker Compose file runs everything: PostgreSQL, MinIO, OpenLDAP, and Silo. An optional nginx container can be enabled for reverse proxying.
### A.1 Prerequisites
- [Docker Engine](https://docs.docker.com/engine/install/) 24+ with the [Compose plugin](https://docs.docker.com/compose/install/) (v2)
- `openssl` (used by the setup script to generate secrets)
Verify your installation:
```bash
docker --version # Docker Engine 24+
docker compose version # Docker Compose v2+
```
### A.2 Clone the Repository
```bash
git clone https://git.kindred-systems.com/kindred/silo.git
cd silo
```
### A.3 Run the Setup Script
The setup script generates credentials and configuration files:
```bash
./scripts/setup-docker.sh
```
It prompts for:
- Server domain (default: `localhost`)
- PostgreSQL password (auto-generated if you press Enter)
- MinIO credentials (auto-generated)
- OpenLDAP admin password and initial user (auto-generated)
- Silo local admin account (fallback when LDAP is unavailable)
For automated/CI environments, use non-interactive mode:
```bash
./scripts/setup-docker.sh --non-interactive
```
The script writes two files:
- `deployments/.env` — secrets for Docker Compose
- `deployments/config.docker.yaml` — Silo server configuration
### A.4 Start the Stack
```bash
docker compose -f deployments/docker-compose.allinone.yaml up -d
```
Wait for all services to become healthy:
```bash
docker compose -f deployments/docker-compose.allinone.yaml ps
```
You should see `silo-postgres`, `silo-minio`, `silo-openldap`, and `silo-api` all in a healthy state.
View logs:
```bash
# All services
docker compose -f deployments/docker-compose.allinone.yaml logs -f
# Silo only
docker compose -f deployments/docker-compose.allinone.yaml logs -f silo
```
### A.5 Verify the Installation
```bash
# Health check
curl http://localhost:8080/health
# Readiness check (includes database and storage connectivity)
curl http://localhost:8080/ready
```
Open http://localhost:8080 in your browser. Log in with either:
- **LDAP account**: the username and password shown by the setup script (default: `siloadmin`)
- **Local admin**: the local admin credentials shown by the setup script (default: `admin`)
The credentials were printed at the end of the setup script output and are stored in `deployments/.env`.
### A.6 LDAP Users and Groups
The Docker stack includes an OpenLDAP server with three preconfigured groups that map to Silo roles:
| LDAP Group | Silo Role | Access Level |
|------------|-----------|-------------|
| `cn=silo-admins,ou=groups,dc=silo,dc=local` | admin | Full access |
| `cn=silo-users,ou=groups,dc=silo,dc=local` | editor | Create and modify items |
| `cn=silo-viewers,ou=groups,dc=silo,dc=local` | viewer | Read-only |
The initial LDAP user (default: `siloadmin`) is added to `silo-admins`.
**Add a new LDAP user:**
```bash
# From the host (using the exposed port)
ldapadd -x -H ldap://localhost:1389 \
-D "cn=admin,dc=silo,dc=local" \
-w "YOUR_LDAP_ADMIN_PASSWORD" << EOF
dn: cn=jdoe,ou=users,dc=silo,dc=local
objectClass: inetOrgPerson
cn: jdoe
sn: Doe
userPassword: changeme
mail: jdoe@example.com
EOF
```
**Add a user to a group:**
```bash
ldapmodify -x -H ldap://localhost:1389 \
-D "cn=admin,dc=silo,dc=local" \
-w "YOUR_LDAP_ADMIN_PASSWORD" << EOF
dn: cn=silo-users,ou=groups,dc=silo,dc=local
changetype: modify
add: member
member: cn=jdoe,ou=users,dc=silo,dc=local
EOF
```
**List all users:**
```bash
ldapsearch -x -H ldap://localhost:1389 \
-b "ou=users,dc=silo,dc=local" \
-D "cn=admin,dc=silo,dc=local" \
-w "YOUR_LDAP_ADMIN_PASSWORD" "(objectClass=inetOrgPerson)" cn mail memberOf
```
### A.7 Optional: Enable Nginx Reverse Proxy
To place nginx in front of Silo (for TLS termination or to serve on port 80):
```bash
docker compose -f deployments/docker-compose.allinone.yaml --profile nginx up -d
```
By default nginx listens on ports 80 and 443 and proxies to the Silo container. The configuration is at `deployments/nginx/nginx.conf`.
**To enable HTTPS**, edit `deployments/docker-compose.allinone.yaml` and uncomment the TLS certificate volume mounts in the `nginx` service, then uncomment the HTTPS server block in `deployments/nginx/nginx.conf`. See the comments in those files for details.
If you already have your own reverse proxy or load balancer, skip the nginx profile and point your proxy at port 8080.
### A.8 Stopping, Starting, and Upgrading
```bash
# Stop the stack (data is preserved in Docker volumes)
docker compose -f deployments/docker-compose.allinone.yaml down
# Start again
docker compose -f deployments/docker-compose.allinone.yaml up -d
# Stop and delete all data (WARNING: destroys database, files, and LDAP data)
docker compose -f deployments/docker-compose.allinone.yaml down -v
```
**To upgrade to a newer version:**
```bash
cd silo
git pull
docker compose -f deployments/docker-compose.allinone.yaml up -d --build
```
The Silo container is rebuilt from the updated source. Database migrations in `migrations/` are applied automatically on container startup via the PostgreSQL init mechanism.
---
## Option B: Daemon Install (systemd + External Services)
This method runs Silo as a systemd service on a dedicated host, connecting to externally managed PostgreSQL, MinIO, and optionally LDAP services.
### B.1 Architecture Overview
```
┌──────────────────────┐
│ Silo Host │
│ ┌────────────────┐ │
HTTPS (443) ──►│ │ nginx │ │
│ └───────┬────────┘ │
│ │ :8080 │
│ ┌───────▼────────┐ │
│ │ silod │ │
│ │ (API server) │ │
│ └──┬─────────┬───┘ │
└─────┼─────────┼──────┘
│ │
┌───────────▼──┐ ┌───▼──────────────┐
│ PostgreSQL 16│ │ MinIO (S3) │
│ :5432 │ │ :9000 API │
└──────────────┘ │ :9001 Console │
└──────────────────┘
```
### B.2 Prerequisites
- Linux host (Debian/Ubuntu or RHEL/Fedora/AlmaLinux)
- Root or sudo access
- Network access to your PostgreSQL and MinIO servers
The setup script installs Go and other build dependencies automatically.
### B.3 Set Up External Services
#### PostgreSQL 16
Install PostgreSQL and create the Silo database:
- [PostgreSQL downloads](https://www.postgresql.org/download/)
```bash
# After installing PostgreSQL, create the database and user:
sudo -u postgres createuser silo
sudo -u postgres createdb -O silo silo
sudo -u postgres psql -c "ALTER USER silo WITH PASSWORD 'your-password';"
```
Ensure the Silo host can connect (check `pg_hba.conf` on the PostgreSQL server).
Verify:
```bash
psql -h YOUR_PG_HOST -U silo -d silo -c 'SELECT 1'
```
#### MinIO
Install MinIO and create a bucket and service account:
- [MinIO quickstart](https://min.io/docs/minio/linux/index.html)
```bash
# Using the MinIO client (mc):
mc alias set local http://YOUR_MINIO_HOST:9000 minioadmin minioadmin
mc mb local/silo-files
mc admin user add local silouser YOUR_MINIO_SECRET
mc admin policy attach local readwrite --user silouser
```
Verify:
```bash
curl -I http://YOUR_MINIO_HOST:9000/minio/health/live
```
#### LDAP / FreeIPA (Optional)
For LDAP authentication, you need an LDAP server with user and group entries. Options:
- [FreeIPA](https://www.freeipa.org/page/Quick_Start_Guide) — full identity management (recommended for organizations already using it)
- [OpenLDAP](https://www.openldap.org/doc/admin26/) — lightweight LDAP server
Silo needs:
- A base DN (e.g., `dc=example,dc=com`)
- Users under a known OU (e.g., `cn=users,cn=accounts,dc=example,dc=com`)
- Groups that map to Silo roles (`admin`, `editor`, `viewer`)
- The `memberOf` overlay enabled (so user entries have `memberOf` attributes)
See [CONFIGURATION.md — LDAP](CONFIGURATION.md#ldap--freeipa) for the full LDAP configuration reference.
### B.4 Prepare the Host
Run the setup script on the target host:
```bash
# Copy and run the script
scp scripts/setup-host.sh root@YOUR_HOST:/tmp/
ssh root@YOUR_HOST 'bash /tmp/setup-host.sh'
```
Or directly on the host:
```bash
sudo bash scripts/setup-host.sh
```
The script:
1. Installs dependencies (git, Go 1.24)
2. Creates the `silo` system user
3. Creates directories (`/opt/silo`, `/etc/silo`)
4. Clones the repository
5. Creates the environment file template
To override the default service hostnames:
```bash
SILO_DB_HOST=db.example.com SILO_MINIO_HOST=s3.example.com sudo -E bash scripts/setup-host.sh
```
### B.5 Configure Credentials
Edit the environment file with your service credentials:
```bash
sudo nano /etc/silo/silod.env
```
```bash
# Database
SILO_DB_PASSWORD=your-database-password
# MinIO
SILO_MINIO_ACCESS_KEY=silouser
SILO_MINIO_SECRET_KEY=your-minio-secret
# Authentication
SILO_SESSION_SECRET=generate-a-long-random-string
SILO_ADMIN_USERNAME=admin
SILO_ADMIN_PASSWORD=your-admin-password
```
Generate a session secret:
```bash
openssl rand -hex 32
```
Review the server configuration:
```bash
sudo nano /etc/silo/config.yaml
```
Update `database.host`, `storage.endpoint`, `server.base_url`, and authentication settings for your environment. See [CONFIGURATION.md](CONFIGURATION.md) for all options.
### B.6 Deploy
Run the deploy script:
```bash
sudo /opt/silo/src/scripts/deploy.sh
```
The script:
1. Pulls latest code from git
2. Builds the `silod` binary and React frontend
3. Installs files to `/opt/silo` and `/etc/silo`
4. Runs database migrations
5. Installs and starts the systemd service
Deploy options:
```bash
# Skip git pull (use current checkout)
sudo /opt/silo/src/scripts/deploy.sh --no-pull
# Skip build (use existing binary)
sudo /opt/silo/src/scripts/deploy.sh --no-build
# Just restart the service
sudo /opt/silo/src/scripts/deploy.sh --restart-only
# Check service status
sudo /opt/silo/src/scripts/deploy.sh --status
```
To override the target host or database host:
```bash
SILO_DEPLOY_TARGET=silo.example.com SILO_DB_HOST=db.example.com sudo -E scripts/deploy.sh
```
### B.7 Set Up Nginx and TLS
#### With FreeIPA (automated)
If your organization uses FreeIPA, the included script handles nginx setup, IPA enrollment, and certificate issuance:
```bash
sudo /opt/silo/src/scripts/setup-ipa-nginx.sh
```
Override the hostname if needed:
```bash
SILO_HOSTNAME=silo.example.com sudo -E /opt/silo/src/scripts/setup-ipa-nginx.sh
```
The script installs nginx, enrolls the host in FreeIPA, requests a TLS certificate from the IPA CA (auto-renewed by certmonger), and configures nginx as an HTTPS reverse proxy.
#### Manual nginx setup
Install nginx and create a config:
```bash
sudo apt install nginx # or: sudo dnf install nginx
```
Use the template at `deployments/nginx/nginx.conf` as a starting point. Copy it to `/etc/nginx/sites-available/silo`, update the `server_name` and certificate paths, then enable it:
```bash
sudo ln -sf /etc/nginx/sites-available/silo /etc/nginx/sites-enabled/silo
sudo nginx -t
sudo systemctl reload nginx
```
After enabling HTTPS, update `server.base_url` in `/etc/silo/config.yaml` to use `https://` and restart Silo:
```bash
sudo systemctl restart silod
```
### B.8 Verify the Installation
```bash
# Service status
sudo systemctl status silod
# Health check
curl http://localhost:8080/health
# Readiness check
curl http://localhost:8080/ready
# Follow logs
sudo journalctl -u silod -f
```
Open your configured base URL in a browser and log in.
### B.9 Upgrading
```bash
# Pull latest code and redeploy
sudo /opt/silo/src/scripts/deploy.sh
# Or deploy a specific version
cd /opt/silo/src
git fetch --all --tags
git checkout v1.2.3
sudo /opt/silo/src/scripts/deploy.sh --no-pull
```
New database migrations are applied automatically during deployment.
---
## Post-Install Configuration
After a successful installation:
- **Authentication**: Configure LDAP, OIDC, or local auth backends. See [CONFIGURATION.md — Authentication](CONFIGURATION.md#authentication).
- **Schemas**: Part numbering schemas are loaded from YAML files. See the `schemas/` directory and [CONFIGURATION.md — Schemas](CONFIGURATION.md#schemas).
- **Read-only mode**: Toggle write protection at runtime with `kill -USR1 $(pidof silod)` or by setting `server.read_only: true` in the config.
- **Ongoing maintenance**: See [DEPLOYMENT.md](DEPLOYMENT.md) for service management, log viewing, troubleshooting, and the security checklist.
---
## Further Reading
| Document | Description |
|----------|-------------|
| [CONFIGURATION.md](CONFIGURATION.md) | Complete `config.yaml` reference |
| [DEPLOYMENT.md](DEPLOYMENT.md) | Operations guide: maintenance, troubleshooting, security |
| [AUTH.md](AUTH.md) | Authentication system design |
| [AUTH_USER_GUIDE.md](AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles |
| [SPECIFICATION.md](SPECIFICATION.md) | Full design specification and API reference |
| [STATUS.md](STATUS.md) | Implementation status |
| [GAP_ANALYSIS.md](GAP_ANALYSIS.md) | Gap analysis and revision control roadmap |
| [COMPONENT_AUDIT.md](COMPONENT_AUDIT.md) | Component audit tool design |

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 11 migrations
- PostgreSQL schema with 13 migrations
- UUID-based identifiers throughout
- Soft delete support via `archived_at` timestamps
- Atomic sequence generation for part numbers
@@ -92,7 +92,7 @@ This document compares Silo's current capabilities against SOLIDWORKS PDM—the
- Template generation for import formatting
#### API & Web Interface
- REST API with 75 endpoints
- REST API with 78 endpoints
- Authentication: local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak
- Role-based access control (admin > editor > viewer)
- API token management (SHA-256 hashed)
@@ -129,7 +129,7 @@ This document compares Silo's current capabilities against SOLIDWORKS PDM—the
| Component | Status |
|-----------|--------|
| PostgreSQL | Running (psql.kindred.internal) |
| PostgreSQL | Running (psql.example.internal) |
| MinIO | Configured in Docker Compose |
| Silo API Server | Builds successfully |
| Docker Compose | Complete (dev and production) |
@@ -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 (75 endpoints) | - | - |
| API access | Full COM/REST API | Full REST API (78 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 (React SPA + auth) | - | - |
**Gap Analysis:**
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.
Silo has a comprehensive REST API (78 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.
---

View File

@@ -37,7 +37,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
┌─────────────────────────────────────────────────────────────┐
│ Silo Server (silod) │
│ - REST API (75 endpoints) │
│ - REST API (78 endpoints) │
│ - Authentication (local, LDAP, OIDC) │
│ - Schema parsing and validation │
│ - Part number generation engine │
@@ -50,7 +50,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ PostgreSQL │ │ MinIO │
│ (psql.kindred.internal)│ │ - File storage │
│ (psql.example.internal)│ │ - File storage │
│ - Item metadata │ │ - Versioned objects │
│ - Relationships │ │ - Thumbnails │
│ - Revision history │ │ │
@@ -63,7 +63,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
| Component | Technology | Notes |
|-----------|------------|-------|
| Database | PostgreSQL 16 | Existing instance at psql.kindred.internal |
| Database | PostgreSQL 16 | Existing instance at psql.example.internal |
| File Storage | MinIO | S3-compatible, versioning enabled |
| CLI & API Server | Go (1.24) | chi/v5 router, pgx/v5 driver, zerolog |
| Authentication | Multi-backend | Local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak |
@@ -598,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 (75 Implemented)
### 11.1 REST Endpoints (78 Implemented)
```
# Health (no auth)
@@ -615,6 +615,9 @@ GET /auth/callback # OIDC callback
# Public API (no auth required)
GET /api/auth/config # Auth backend configuration (for login UI)
# Server-Sent Events (require auth)
GET /api/events # SSE stream for real-time updates
# Auth API (require auth)
GET /api/auth/me # Current authenticated user
GET /api/auth/tokens # List user's API tokens
@@ -627,7 +630,7 @@ POST /api/uploads/presign # Get presigned MinI
# Schemas (read: viewer, write: editor)
GET /api/schemas # List all schemas
GET /api/schemas/{name} # Get schema details
GET /api/schemas/{name}/properties # Get property schema for category
GET /api/schemas/{name}/form # Get form descriptor (field groups, widgets, category picker)
POST /api/schemas/{name}/segments/{segment}/values # Add enum value [editor]
PUT /api/schemas/{name}/segments/{segment}/values/{code} # Update enum value [editor]
DELETE /api/schemas/{name}/segments/{segment}/values/{code} # Delete enum value [editor]
@@ -644,6 +647,7 @@ DELETE /api/projects/{code} # Delete project [ed
# Items (read: viewer, write: editor)
GET /api/items # List/filter items
GET /api/items/search # Fuzzy search
GET /api/items/by-uuid/{uuid} # Get item by UUID
GET /api/items/export.csv # Export items to CSV
GET /api/items/template.csv # CSV import template
GET /api/items/export.ods # Export items to ODS
@@ -689,6 +693,7 @@ GET /api/items/{partNumber}/bom/export.csv # Export BOM as CSV
GET /api/items/{partNumber}/bom/export.ods # Export BOM as ODS
POST /api/items/{partNumber}/bom # Add BOM entry [editor]
POST /api/items/{partNumber}/bom/import # Import BOM from CSV [editor]
POST /api/items/{partNumber}/bom/merge # Merge BOM from ODS with conflict resolution [editor]
PUT /api/items/{partNumber}/bom/{childPartNumber} # Update BOM entry [editor]
DELETE /api/items/{partNumber}/bom/{childPartNumber} # Remove BOM entry [editor]
@@ -734,11 +739,11 @@ POST /api/inventory/{partNumber}/move
### 12.1 Implemented
- [x] PostgreSQL database schema (11 migrations)
- [x] PostgreSQL database schema (13 migrations)
- [x] YAML schema parser for part numbering
- [x] Part number generation engine
- [x] CLI tool (`cmd/silo`)
- [x] API server (`cmd/silod`) with 75 endpoints
- [x] API server (`cmd/silod`) with 78 endpoints
- [x] MinIO integration for file storage with versioning
- [x] BOM relationships (component, alternate, reference)
- [x] Multi-level BOM (recursive expansion with configurable depth)

View File

@@ -10,10 +10,10 @@
| Component | Status | Notes |
|-----------|--------|-------|
| PostgreSQL schema | Complete | 11 migrations applied |
| PostgreSQL schema | Complete | 13 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 | 75 REST endpoints via chi/v5 |
| API server (`silod`) | Complete | 78 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 |
@@ -55,7 +55,7 @@ FreeCAD workbench and LibreOffice Calc extension are maintained in separate repo
| Service | Host | Status |
|---------|------|--------|
| PostgreSQL | psql.kindred.internal:5432 | Running |
| PostgreSQL | psql.example.internal:5432 | Running |
| MinIO | localhost:9000 (API) / :9001 (console) | Configured |
| Silo API | localhost:8080 | Builds successfully |
@@ -92,5 +92,7 @@ The schema defines 170 category codes across 10 groups:
| 007_revision_status.sql | Revision status and labels |
| 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) |
| 010_item_extended_fields.sql | Extended item fields (sourcing_type, long_description) |
| 011_item_files.sql | Item file attachments (item_files table, thumbnail_key column) |
| 012_bom_source.sql | BOM entry source tracking |
| 013_move_cost_sourcing_to_props.sql | Move sourcing_link and standard_cost from item columns to revision properties |

View File

@@ -1,6 +1,6 @@
# Silo Frontend Specification
Current as of 2026-02-08. Documents the React + Vite + TypeScript frontend (migration from Go templates is complete).
Current as of 2026-02-11. Documents the React + Vite + TypeScript frontend (migration from Go templates is complete).
## Overview
@@ -68,6 +68,7 @@ web/
│ └── 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/
@@ -271,63 +272,81 @@ Vite dev server runs on port 5173 with proxy config in `vite.config.ts` forwardi
## New Frontend Tasks
# CreateItemPane Redesign Specification
# CreateItemPane — Schema-Driven Dynamic Form
**Date**: 2026-02-06
**Scope**: Replace existing `CreateItemPane.tsx` with a two-column layout, multi-stage category picker, file attachment via MinIO, and full use of screen real estate.
**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
The pane uses a CSS Grid two-column layout instead of the current single-column form:
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 │
├──────────────────────────────────────────────────────┤ │
│ Auto-
── Identity ────────────────────────────────────── │ assigned
[Part Number *] [Type * v] │ metadata
[Description ] │
Category * [Domain │ Group │ Subtype ] │──────────────
Mechanical│ Structural│ Bracket │ │
Electrical│ Bearings │ Plate │ │ Attachments
... │ ... │ ... │ │ ┌─ ─ ─ ─ ┐
── Sourcing ────────────────────────────────────── │ │ Drop
│ [Sourcing Type v] [Standard Cost $ ] │ zone │
[Unit of Measure v] [Sourcing Link ] └─ ─ ─ ─ ┘
│ file.FCStd
── Details ─────────────────────────────────────── │ drawing.pdf
[Long Description ]
[Projects: [tag][tag] type to search... ] │──────────────│
│ Thumbnail
│ [preview]
──────────────────────────────────────────────────────┴──────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 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] ... │
│ │
└──────────────────────────────────────────────────────────────────────┘
```
Grid definition: `grid-template-columns: 1fr 320px`. The left column scrolls independently if content overflows. The right sidebar is a flex column with sections separated by `--ctp-surface1` borders.
## 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` (replaces existing file)
`web/src/components/items/CreateItemPane.tsx`
New supporting files:
Supporting files:
| File | Purpose |
|------|---------|
| `web/src/components/items/CategoryPicker.tsx` | Multi-stage category selector |
| `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/useCategories.ts` | Fetches category tree from schema data |
| `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. Manages form state, submission, and layout.
Top-level orchestrator. Renders dynamic form from the form descriptor.
**Props** (unchanged interface):
@@ -341,68 +360,64 @@ interface CreateItemPaneProps {
**State**:
```typescript
const [form, setForm] = useState<CreateItemForm>({
part_number: '',
item_type: 'part',
description: '',
category_path: [], // e.g. ['Mechanical', 'Structural', 'Bracket']
sourcing_type: 'manufactured',
standard_cost: '',
unit_of_measure: 'ea',
sourcing_link: '',
long_description: '',
project_ids: [],
});
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
const [thumbnail, setThumbnail] = useState<PendingAttachment | null>(null);
const { descriptor, categories, loading } = useFormDescriptor();
const [category, setCategory] = useState(''); // selected category code, e.g. "F01"
const [fields, setFields] = useState<Record<string, string>>({}); // all field values keyed by name
const [error, setError] = useState<string | null>(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` | `<input type="text">` |
| `number` | `<input type="number">` |
| `textarea` | `<textarea>` |
| `select` | `<select>` with `<option>` elements from `field.options` |
| `checkbox` | `<input type="checkbox">` |
| `currency` | `<input type="number">` with currency prefix (e.g., "$") |
| `url` | `<input type="url">` |
| `tag_input` | `TagInput` component with search endpoint |
**Submission flow**:
1. Validate required fields (part_number, item_type, category_path length === 3).
2. `POST /api/items` with form data → returns created `Item` with UUID.
3. For each attachment in `attachments[]`, call the file association endpoint: `POST /api/items/{id}/files` with the MinIO object key returned from upload.
4. If thumbnail exists, `PUT /api/items/{id}/thumbnail` with the object key.
5. Call `onCreated(item)`.
1. Validate required fields (category must be selected).
2. Split `fields` into item-level fields and properties using `ITEM_LEVEL_FIELDS`.
3. `POST /api/items` with `{ part_number: '', item_type, description, sourcing_type, long_description, category, properties: {...} }`.
4. Call `onCreated(item)`.
If step 2 fails, show error banner. If file association fails, show warning but still navigate (item was created, files can be re-attached).
**Header bar**: Green (`--ctp-green` background, `--ctp-crust` text) per existing create-pane convention. "New Item" title on left, Cancel (ghost button) and Create Item (primary button, `--ctp-green` bg) on right.
**Header bar**: Green (`--ctp-green` background, `--ctp-crust` text). "New Item" title on left, Cancel and Create Item buttons on right.
### CategoryPicker
Three-column scrollable list for hierarchical category selection.
Multi-stage category selector driven by the form descriptor's `category_picker.stages` config.
**Props**:
```typescript
interface CategoryPickerProps {
value: string[]; // current selection path, e.g. ['Mechanical', 'Structural']
onChange: (path: string[]) => void;
categories: CategoryNode[]; // top-level nodes
}
interface CategoryNode {
name: string;
children?: CategoryNode[];
value: string; // selected category code, e.g. "F01"
onChange: (code: string) => void;
categories: Record<string, string>; // flat code → description map
stages?: CategoryPickerStage[]; // from form descriptor
}
```
**Rendering**: Three side-by-side `<div>` columns inside a container with `border: 1px solid var(--ctp-surface1)` and `border-radius: 0.4rem`. Each column has:
**Rendering**: Two-stage selection:
- A sticky header row (10px uppercase, `--ctp-overlay0` text, `--ctp-mantle` background) labeling the tier. Labels come from the schema definition if available, otherwise "Level 1", "Level 2", "Level 3".
- A scrollable list of options. Each option is a `<div>` row, 28px height, `0.85rem` font. Hover: `--ctp-surface0` background. Selected: translucent mauve background (`rgba(203, 166, 247, 0.12)`), `--ctp-mauve` text, weight 600.
- If a node has children, show a `` chevron on the right side of the row.
1. **Domain row**: Horizontal row of buttons, one per domain from `stages[0].values` (F=Fasteners, C=Fluid Fittings, etc.). Selected domain has mauve highlight.
2. **Subcategory list**: Filtered list of categories matching the selected domain prefix. Includes a search input for filtering. Each row shows code and description.
Column 1 always shows all top-level nodes. Column 2 shows children of the selected Column 1 node. Column 3 shows children of the selected Column 2 node. If nothing is selected in a column, the next column shows an empty state with muted text: "Select a [tier name]".
If no `stages` prop is provided, falls back to a flat searchable list of all categories.
Below the picker, render a breadcrumb trail: `Mechanical Structural Bracket` in `--ctp-mauve` with `` separators in `--ctp-overlay0`. Only show segments that are selected.
Below the picker, the selected category is shown as a breadcrumb: `Fasteners F01 — Hex Cap Screw` in `--ctp-mauve`.
**Data source**: Categories are derived from schemas. The `useCategories` hook calls `GET /api/schemas` and transforms the response into a `CategoryNode[]` tree. The exact mapping depends on how schemas define category hierarchies — if schemas don't currently support hierarchical categories, this requires a backend addition (see Backend Changes section).
**Max height**: 180px per column with `overflow-y: auto`.
**Data source**: Categories come from `useFormDescriptor()` which derives them from the `category_picker` stages and `values_by_domain` in the form descriptor response.
### FileDropZone
@@ -478,17 +493,17 @@ The dropdown is an absolutely-positioned `<div>` below the input container, `--c
**For projects**: `searchFn` calls `GET /api/projects?q={query}` and maps to `{ id: project.id, label: project.code + ' — ' + project.name }`.
### useCategories Hook
### useFormDescriptor Hook
```typescript
function useCategories(): {
categories: CategoryNode[];
function useFormDescriptor(schemaName = "kindred-rd"): {
descriptor: FormDescriptor | null;
categories: Record<string, string>; // flat code → description map derived from descriptor
loading: boolean;
error: string | null;
}
```
Fetches `GET /api/schemas` on mount and transforms into a category tree. Caches in a module-level variable so repeated renders don't refetch. If the API doesn't currently support hierarchical categories, this returns a flat list as a single-tier picker until the backend is extended.
Fetches `GET /api/schemas/{name}/form` on mount. Caches the result in a module-level variable so repeated renders/mounts don't refetch. Derives a flat `categories` map from the `category_picker` stages and `values_by_domain` in the response. Replaces the old `useCategories` hook (deleted).
### useFileUpload Hook
@@ -542,30 +557,32 @@ const styles = {
## Form Sections
The form is visually divided by section headers. Each header is a flex row containing a label (11px uppercase, `--ctp-overlay0`) and a `flex: 1` horizontal line (`1px solid --ctp-surface0`). Sections span `grid-column: 1 / -1`.
Form sections are rendered dynamically from the `field_groups` array in the form descriptor. Each section header is a flex row containing a label (11px uppercase, `--ctp-overlay0`) and a `flex: 1` horizontal line (`1px solid --ctp-surface0`).
| Section | Fields |
|---------|--------|
| Identity | Part Number*, Type*, Description, Category* |
| Sourcing | Sourcing Type, Standard Cost, Unit of Measure, Sourcing Link |
| Details | Long Description, Projects |
**Global field groups** (from `ui.field_groups` in YAML):
## Sidebar Sections
| Group Key | Label | Fields |
|-----------|-------|--------|
| identity | Identity | item_type, description |
| sourcing | Sourcing | sourcing_type, manufacturer, manufacturer_pn, supplier, supplier_pn, sourcing_link |
| cost | Cost & Lead Time | standard_cost, lead_time_days, minimum_order_qty |
| status | Status | lifecycle_status, rohs_compliant, country_of_origin |
| details | Details | long_description, projects, notes |
The right sidebar is divided into three sections with `borderBottom: 1px solid var(--ctp-surface0)`:
**Category-specific field groups** (from `ui.category_field_groups` in YAML, shown when a category is selected):
**Auto-assigned metadata**: Read-only key-value rows showing:
- UUID: "On create" in `--ctp-teal` italic
- Revision: "A" (hardcoded initial)
- Created By: current user's display name from `useAuth()`
| Prefix | Group | Example Fields |
|--------|-------|----------------|
| F | Fastener Specifications | material, finish, thread_size, head_type, drive_type, ... |
| C | Fitting Specifications | material, connection_type, size_1, pressure_rating, ... |
| R | Motion Specifications | bearing_type, bore_diameter, load_rating, ... |
| ... | ... | (one group per category prefix, defined in YAML) |
**Attachments**: `FileDropZone` component. Takes `flex: 1` to fill available space.
**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`.
Note: `sourcing_link` and `standard_cost` are revision properties (stored in the `properties` JSONB), not item-level DB columns. They were migrated from item-level fields in PR #1 (migration 013).
## Backend Changes
Items 1-3 and 5 below are implemented (migration `011_item_files.sql`, `internal/api/file_handlers.go`). Item 4 (hierarchical categories) remains open.
Items 1-5 below are implemented. Item 4 (hierarchical categories) is resolved by the form descriptor's multi-stage category picker.
### 1. Presigned Upload URL -- IMPLEMENTED
@@ -597,33 +614,14 @@ Response: 204
Stores the thumbnail at `items/{item_id}/thumbnail.png` in MinIO. Updates `item.thumbnail_key` column.
### 4. Hierarchical Categories -- NOT IMPLEMENTED
### 4. Hierarchical Categories -- IMPLEMENTED (via Form Descriptor)
If schemas don't currently support a hierarchical category tree, one of these approaches:
Resolved by the schema-driven form descriptor (`GET /api/schemas/{name}/form`). The YAML schema's `ui.category_picker` section defines multi-stage selection:
**Option A — Schema-driven**: Add a `category_tree` JSON column to the `schemas` table that defines the hierarchy. The `GET /api/schemas` response already returns schemas; the frontend transforms this into the picker tree.
- **Stage 1 (domain)**: Groups categories by first character of category code (F=Fasteners, C=Fluid Fittings, etc.). Values defined in `ui.category_picker.stages[0].values`.
- **Stage 2 (subcategory)**: Auto-derived by the Go backend's `ValuesByDomain()` method, which groups the category enum values by their first character.
**Option B — Dedicated table**:
```sql
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
parent_id UUID REFERENCES categories(id),
sort_order INT NOT NULL DEFAULT 0,
UNIQUE(parent_id, name)
);
```
With endpoints:
```
GET /api/categories → flat list with parent_id, frontend builds tree
POST /api/categories → { name, parent_id? }
PUT /api/categories/{id} → { name, sort_order }
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).
No separate `categories` table is needed — the existing schema enum values are the single source of truth. Adding a new category code to the YAML propagates to the picker automatically.
### 5. Database Schema Addition -- IMPLEMENTED
@@ -641,46 +639,89 @@ CREATE TABLE item_files (
CREATE INDEX idx_item_files_item ON item_files(item_id);
ALTER TABLE items ADD COLUMN thumbnail_key TEXT;
ALTER TABLE items ADD COLUMN category_id UUID REFERENCES categories(id);
ALTER TABLE items ADD COLUMN sourcing_type TEXT NOT NULL DEFAULT 'manufactured';
ALTER TABLE items ADD COLUMN sourcing_link TEXT;
ALTER TABLE items ADD COLUMN standard_cost NUMERIC(12,2);
ALTER TABLE items ADD COLUMN unit_of_measure TEXT NOT NULL DEFAULT 'ea';
ALTER TABLE items ADD COLUMN long_description TEXT;
```
## Implementation Order
1. **TagInput component**reusable, no backend changes needed, uses existing projects API.
2. **CategoryPicker component**start with flat/mock data, wire to real API after backend adds categories.
3. **FileDropZone + useFileUpload**requires presigned URL backend endpoint first.
4. **CreateItemPane rewrite**compose the above into the two-column layout.
5. **Backend: categories table + endpoints** — unblocks real category data.
6. **Backend: presigned uploads + item_files** — unblocks file attachments.
7. **Backend: items table migration** — adds new columns (sourcing_type, standard_cost, etc.).
1. **[DONE] Deduplicate sourcing_link/standard_cost** — Migrated from item-level DB columns to revision properties (migration 013). Removed from Go structs, API types, frontend types.
2. **[DONE] Form descriptor API** — Added `ui` section to YAML, Go structs + validation, `GET /api/schemas/{name}/form` endpoint.
3. **[DONE] useFormDescriptor hook** — Replaces `useCategories`, fetches and caches form descriptor.
4. **[DONE] CategoryPicker rewrite** — Multi-stage domain/subcategory picker driven by form descriptor.
5. **[DONE] CreateItemPane rewrite** — Dynamic form rendering from field groups, widget-based field rendering.
6. **TagInput component** — reusable, no backend changes needed, uses existing projects API.
7. **FileDropZone + useFileUpload** — requires presigned URL backend endpoint (already implemented).
Steps 1-2 can start immediately. Steps 5-7 can run in parallel once specified. Step 4 ties it all together.
## Types Added
## Types to Add
Add to `web/src/api/types.ts`:
The following types were added to `web/src/api/types.ts` for the form descriptor system:
```typescript
// Categories
interface Category {
id: string;
// Form descriptor types (from GET /api/schemas/{name}/form)
interface FormFieldDescriptor {
name: string;
parent_id: string | null;
sort_order: number;
type: string;
widget: string;
label: string;
required?: boolean;
default?: string;
unit?: string;
description?: string;
options?: string[];
currency?: string;
derived_from_category?: Record<string, string>;
search_endpoint?: string;
}
interface CategoryNode {
name: string;
id: string;
children?: CategoryNode[];
interface FormFieldGroup {
key: string;
label: string;
order: number;
fields: FormFieldDescriptor[];
}
// File uploads
interface CategoryPickerStage {
name: string;
label: string;
values?: Record<string, string>;
values_by_domain?: Record<string, Record<string, string>>;
}
interface CategoryPickerDescriptor {
style: string;
stages: CategoryPickerStage[];
}
interface ItemFieldDef {
type: string;
widget: string;
label: string;
required?: boolean;
default?: string;
options?: string[];
derived_from_category?: Record<string, string>;
search_endpoint?: string;
}
interface FieldOverride {
widget?: string;
currency?: string;
options?: string[];
}
interface FormDescriptor {
schema_name: string;
format: string;
category_picker: CategoryPickerDescriptor;
item_fields: Record<string, ItemFieldDef>;
field_groups: FormFieldGroup[];
category_field_groups: Record<string, FormFieldGroup[]>;
field_overrides: Record<string, FieldOverride>;
}
// File uploads (unchanged)
interface PresignRequest {
filename: string;
content_type: string;
@@ -703,20 +744,6 @@ interface ItemFile {
created_at: string;
}
// Extended create request
interface CreateItemRequest {
part_number: string;
item_type: 'part' | 'assembly' | 'document';
description?: string;
category_id?: string;
sourcing_type?: 'manufactured' | 'purchased' | 'phantom';
standard_cost?: number;
unit_of_measure?: string;
sourcing_link?: string;
long_description?: string;
project_ids?: string[];
}
// Pending upload (frontend only, not an API type)
interface PendingAttachment {
file: File;
@@ -726,3 +753,5 @@ interface PendingAttachment {
error?: string;
}
```
Note: `sourcing_link` and `standard_cost` have been removed from the `Item`, `CreateItemRequest`, and `UpdateItemRequest` interfaces — they are now stored as revision properties and rendered dynamically from the form descriptor.

View File

@@ -25,7 +25,7 @@ silo/
│ ├── silo/ # CLI tool
│ └── silod/ # API server
├── internal/
│ ├── api/ # HTTP handlers and routes (75 endpoints)
│ ├── api/ # HTTP handlers and routes (78 endpoints)
│ ├── auth/ # Authentication (local, LDAP, OIDC)
│ ├── config/ # Configuration loading
│ ├── db/ # PostgreSQL repositories
@@ -53,15 +53,20 @@ silo/
## Quick Start
```bash
# Docker Compose (quickest)
cp config.example.yaml config.yaml
# Edit config.yaml with your database, MinIO, and auth settings
make docker-up
See the **[Installation Guide](docs/INSTALL.md)** for complete setup instructions.
# Or manual setup
psql -h localhost -U silo -d silo -f migrations/*.sql
go run ./cmd/silod -config config.yaml
**Docker Compose (quickest — includes PostgreSQL, MinIO, OpenLDAP, and Silo):**
```bash
./scripts/setup-docker.sh
docker compose -f deployments/docker-compose.allinone.yaml up -d
```
**Development (local Go + Docker services):**
```bash
make docker-up # Start PostgreSQL + MinIO in Docker
make run # Run silo locally with Go
```
When auth is enabled, a default admin account is created on first startup using the credentials in `config.yaml` under `auth.local.default_admin_username` and `auth.local.default_admin_password`.
@@ -104,9 +109,10 @@ The server provides the REST API and ODS endpoints consumed by these clients.
| Document | Description |
|----------|-------------|
| [docs/INSTALL.md](docs/INSTALL.md) | Installation guide (Docker Compose and daemon) |
| [docs/SPECIFICATION.md](docs/SPECIFICATION.md) | Full design specification and API reference |
| [docs/STATUS.md](docs/STATUS.md) | Implementation status |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment and operations 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 |

View File

@@ -34,53 +34,67 @@ except ImportError:
# Catppuccin Mocha colors
COLORS = {
'base': '#1e1e2e',
'surface0': '#313244',
'text': '#cdd6f4',
'subtext0': '#a6adc8',
'blue': '#89b4fa',
'lavender': '#b4befe',
"mantle": "#181825",
"base": "#1e1e2e",
"surface0": "#313244",
"text": "#cdd6f4",
"subtext0": "#a6adc8",
"blue": "#89b4fa",
"lavender": "#b4befe",
}
def hex_to_rgb(hex_color):
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
def create_rounded_rectangle(draw, bbox, radius, fill):
"""Draw a rounded rectangle."""
x1, y1, x2, y2 = bbox
draw.rounded_rectangle(bbox, radius=radius, fill=fill)
def load_svg_as_image(svg_path, width, height):
"""Load an SVG file and convert to PIL Image at specified size."""
if cairosvg:
import io
png_data = cairosvg.svg2png(url=str(svg_path), output_width=width, output_height=height)
return Image.open(io.BytesIO(png_data)).convert('RGBA')
return Image.open(io.BytesIO(png_data)).convert("RGBA")
else:
# Fallback: try using inkscape command line
import subprocess
import tempfile
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
tmp_path = tmp.name
try:
subprocess.run([
'inkscape', '-w', str(width), '-h', str(height),
str(svg_path), '-o', tmp_path
], check=True, capture_output=True)
img = Image.open(tmp_path).convert('RGBA')
subprocess.run(
["inkscape", "-w", str(width), "-h", str(height), str(svg_path), "-o", tmp_path],
check=True,
capture_output=True,
)
img = Image.open(tmp_path).convert("RGBA")
return img
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
def get_font(size, bold=False):
"""Get a font, falling back to default if not available."""
font_names = [
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf' if bold else '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf' if bold else '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
'/usr/share/fonts/TTF/DejaVuSans-Bold.ttf' if bold else '/usr/share/fonts/TTF/DejaVuSans.ttf',
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
if bold
else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"
if bold
else "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
if bold
else "/usr/share/fonts/TTF/DejaVuSans.ttf",
]
for font_name in font_names:
if os.path.exists(font_name):
@@ -88,6 +102,7 @@ def get_font(size, bold=False):
# Fallback to default
return ImageFont.load_default()
def create_splash(output_path, logo_path, width, height, version, freecad_version, scale=1):
"""Create a splash screen image."""
# Scale dimensions
@@ -97,11 +112,11 @@ def create_splash(output_path, logo_path, width, height, version, freecad_versio
padding = int(30 * scale)
# Create image with transparent background
img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Draw rounded rectangle background
bg_color = hex_to_rgb(COLORS['base'])
bg_color = hex_to_rgb(COLORS["mantle"])
create_rounded_rectangle(draw, (padding, padding, w - padding, h - padding), radius, bg_color)
# Load and place logo
@@ -125,7 +140,7 @@ def create_splash(output_path, logo_path, width, height, version, freecad_versio
title_width = title_bbox[2] - title_bbox[0]
title_x = (w - title_width) // 2
title_y = int(250 * scale)
draw.text((title_x, title_y), title, fill=hex_to_rgb(COLORS['text']), font=title_font)
draw.text((title_x, title_y), title, fill=hex_to_rgb(COLORS["text"]), font=title_font)
# Draw version string
version_font = get_font(int(12 * scale))
@@ -134,12 +149,15 @@ def create_splash(output_path, logo_path, width, height, version, freecad_versio
version_width = version_bbox[2] - version_bbox[0]
version_x = (w - version_width) // 2
version_y = title_y + int(35 * scale)
draw.text((version_x, version_y), version_str, fill=hex_to_rgb(COLORS['subtext0']), font=version_font)
draw.text(
(version_x, version_y), version_str, fill=hex_to_rgb(COLORS["subtext0"]), font=version_font
)
# Save
img.save(output_path, 'PNG')
img.save(output_path, "PNG")
print(f"Created: {output_path}")
def create_about(output_path, logo_path, width, height, scale=1):
"""Create an about dialog image."""
# Scale dimensions
@@ -147,7 +165,7 @@ def create_about(output_path, logo_path, width, height, scale=1):
h = int(height * scale)
# Create image
img = Image.new('RGBA', (w, h), hex_to_rgb(COLORS['base']) + (255,))
img = Image.new("RGBA", (w, h), hex_to_rgb(COLORS["base"]) + (255,))
draw = ImageDraw.Draw(img)
# Load and place logo
@@ -163,18 +181,19 @@ def create_about(output_path, logo_path, width, height, scale=1):
print(f"Warning: Could not load logo: {e}")
# Save
img.save(output_path, 'PNG')
img.save(output_path, "PNG")
print(f"Created: {output_path}")
def main():
parser = argparse.ArgumentParser(description='Generate Kindred Create splash screens')
parser.add_argument('--version', default='0.1.0', help='Kindred Create version')
parser.add_argument('--freecad-version', default='1.0.0', help='FreeCAD version')
parser = argparse.ArgumentParser(description="Generate Kindred Create splash screens")
parser.add_argument("--version", default="0.1.0", help="Kindred Create version")
parser.add_argument("--freecad-version", default="1.0.0", help="FreeCAD version")
args = parser.parse_args()
script_dir = Path(__file__).parent
logo_path = script_dir / 'kindred-logo.svg'
icons_dir = script_dir.parent.parent / 'src' / 'Gui' / 'Icons'
logo_path = script_dir / "kindred-logo.svg"
icons_dir = script_dir.parent.parent / "src" / "Gui" / "Icons"
if not logo_path.exists():
print(f"Error: Logo not found at {logo_path}")
@@ -182,33 +201,31 @@ def main():
# Create splash screens (600x400 as per spec)
create_splash(
icons_dir / 'kindredcreatesplash.png',
icons_dir / "kindredcreatesplash.png",
logo_path,
600, 400,
600,
400,
args.version,
args.freecad_version,
scale=1
scale=1,
)
# Create 2x version for HiDPI
create_splash(
icons_dir / 'kindredcreatesplash_2x.png',
icons_dir / "kindredcreatesplash_2x.png",
logo_path,
600, 400,
600,
400,
args.version,
args.freecad_version,
scale=2
scale=2,
)
# Create about image
create_about(
icons_dir / 'kindredcreateabout.png',
logo_path,
400, 200,
scale=1
)
create_about(icons_dir / "kindredcreateabout.png", logo_path, 400, 200, scale=1)
print("\nSplash screen generation complete!")
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -2271,7 +2271,7 @@ PyObject* ApplicationPy::sRegisterEditingContext(PyObject* /*self*/, PyObject* a
}
catch (Py::Exception&) {
Base::PyException e;
e.ReportException();
e.reportException();
return false;
}
};
@@ -2322,7 +2322,7 @@ PyObject* ApplicationPy::sRegisterEditingOverlay(PyObject* /*self*/, PyObject* a
}
catch (Py::Exception&) {
Base::PyException e;
e.ReportException();
e.reportException();
return false;
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -360,9 +360,6 @@ QPixmap SplashScreen::splashImage()
fontExe.setPointSizeF(20.0);
QFontMetrics metricExe(fontExe);
int l = QtTools::horizontalAdvance(metricExe, title);
if (title == QLatin1String("Kindred Create")) {
// For Kindred Create splash, we draw the title as part of the dynamic rendering
}
int w = splash_image.width();
int h = splash_image.height();
@@ -386,13 +383,16 @@ QPixmap SplashScreen::splashImage()
QColor color(QString::fromStdString(tc->second));
if (color.isValid()) {
painter.setPen(color);
painter.setFont(fontExe);
if (title != QLatin1String("FreeCAD")) {
// FreeCAD's Splashscreen already contains the EXE name, no need to draw it
painter.drawText(x, y, title);
if (title != QLatin1String("Kindred Create")) {
// Kindred Create's splash PNG already contains the title and version
painter.setFont(fontExe);
if (title != QLatin1String("FreeCAD")) {
// FreeCAD's Splashscreen already contains the EXE name, no need to draw it
painter.drawText(x, y, title);
}
painter.setFont(fontVer);
painter.drawText(x + (l + 235), y - 7, version);
}
painter.setFont(fontVer);
painter.drawText(x + (l + 235), y - 7, version);
QColor warningColor(QString::fromStdString(wc->second));
if (suffix == QLatin1String("dev") && warningColor.isValid()) {
fontVer.setPointSizeF(14.0);

View File

@@ -29,6 +29,7 @@
#include <Base/BaseClass.h>
#include <Base/Parameter.h>
#include <Gui/TaskView/TaskWatcher.h>
#include <Gui/ToolBarManager.h>
namespace Base
{
@@ -39,7 +40,6 @@ namespace Gui
{
class MenuItem;
class ToolBarItem;
class DockWindowItems;
class WorkbenchManager;