phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)

Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
This commit is contained in:
forbes
2026-02-13 14:03:58 -06:00
parent 5d81f8ac16
commit 87a0af0b0f
1566 changed files with 32071 additions and 6155 deletions

View File

@@ -0,0 +1,82 @@
name: Bug Report
about: Report a bug or unexpected behavior in Kindred Create
labels:
- bug
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear description of the bug.
placeholder: What happened? What did you expect instead?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: Minimal steps to reproduce the behavior.
placeholder: |
1. Open a new file
2. Switch to the PartDesign workbench
3. ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What should have happened?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened? Include error messages, console output, or screenshots.
validations:
required: true
- type: dropdown
id: component
attributes:
label: Component
description: Which part of Kindred Create is affected?
options:
- General / Core
- ztools Workbench
- Silo (Parts Database)
- Theme / QSS
- Assembly
- PartDesign
- CI/CD
- Documentation
- Other
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: |
OS, Kindred Create version, and any relevant details.
Find the version under Help > About Kindred Create.
placeholder: |
- OS: Ubuntu 24.04
- Kindred Create: v0.2.0 (AppImage)
- GPU: NVIDIA RTX 3060
validations:
required: false
- type: textarea
id: logs
attributes:
label: Log Output
description: Paste any relevant log output (View > Panels > Report View).
render: shell
validations:
required: false

View File

@@ -0,0 +1,42 @@
name: Documentation
about: Report missing, incorrect, or unclear documentation
labels:
- documentation
body:
- type: textarea
id: description
attributes:
label: Description
description: What needs to be documented, updated, or corrected?
validations:
required: true
- type: dropdown
id: type
attributes:
label: Type
description: What kind of documentation change is needed?
options:
- Missing documentation
- Incorrect / outdated content
- Unclear or confusing
- New guide or tutorial
validations:
required: true
- type: textarea
id: location
attributes:
label: Location
description: Where should this documentation live? Link to existing pages if applicable.
placeholder: e.g. docs/src/guide/ztools.md, or "new page under Reference"
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional Context
description: Any other details, screenshots, or references that would help.
validations:
required: false

View File

@@ -0,0 +1,55 @@
name: Feature Request
about: Suggest a new feature or enhancement for Kindred Create
labels:
- enhancement
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear description of the feature you'd like.
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use Case
description: Why is this feature needed? What problem does it solve?
placeholder: As a user, I want to... so that...
validations:
required: true
- type: dropdown
id: component
attributes:
label: Component
description: Which part of Kindred Create does this relate to?
options:
- General / Core
- ztools Workbench
- Silo (Parts Database)
- Theme / QSS
- Assembly
- PartDesign
- CI/CD
- Documentation
- Other
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed Implementation
description: If you have ideas on how this could be implemented, describe them here.
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any alternative approaches or workarounds you've considered.
validations:
required: false

View File

@@ -0,0 +1,18 @@
## Summary
<!-- Brief description of what this PR does -->
## Changes
<!-- List the key changes made -->
## Related Issues
<!-- Link related issues: Closes #123, Fixes #456 -->
## Checklist
- [ ] Commit messages follow [conventional commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, `docs:`, `art:`)
- [ ] Code follows project style (clang-format for C++, black for Python)
- [ ] Changes are tested locally
- [ ] Documentation updated (if applicable)

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Kindred Create CI runner disk cleanup
After=docker.service
[Service]
Type=oneshot
ExecStart=/opt/runner/cleanup.sh
Environment=CLEANUP_THRESHOLD=85
Environment=CACHE_MAX_AGE_DAYS=7
StandardOutput=append:/var/log/runner-cleanup.log
StandardError=append:/var/log/runner-cleanup.log

220
.gitea/runner/cleanup.sh Executable file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env bash
# Runner disk cleanup script for Kindred Create CI/CD
#
# Designed to run as a cron job on the pubworker host:
# */30 * * * * /path/to/cleanup.sh >> /var/log/runner-cleanup.log 2>&1
#
# Or install the systemd timer (see cleanup.timer / cleanup.service).
#
# What it cleans:
# 1. Docker: stopped containers, dangling images, build cache
# 2. act_runner action cache: keeps only the newest entry per key prefix
# 3. act_runner workspaces: removes leftover build workspaces
# 4. System: apt cache, old logs
#
# What it preserves:
# - The current runner container and its image
# - The most recent cache entry per prefix (so ccache hits still work)
# - Everything outside of known CI paths
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration -- adjust these to match your runner setup
# ---------------------------------------------------------------------------
# Disk usage threshold (percent) -- only run aggressive cleanup above this
THRESHOLD=${CLEANUP_THRESHOLD:-85}
# act_runner cache directory (default location)
CACHE_DIR=${CACHE_DIR:-/root/.cache/actcache}
# act_runner workspace directories
WORKSPACES=(
"/root/.cache/act"
"/workspace"
)
# Maximum age (days) for cache entries before unconditional deletion
CACHE_MAX_AGE_DAYS=${CACHE_MAX_AGE_DAYS:-7}
# Maximum age (days) for Docker images not used by running containers
DOCKER_IMAGE_MAX_AGE=${DOCKER_IMAGE_MAX_AGE:-48h}
# Log prefix
LOG_PREFIX="[runner-cleanup]"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') ${LOG_PREFIX} $*"; }
disk_usage_pct() {
df --output=pcent / | tail -1 | tr -dc '0-9'
}
bytes_to_human() {
numfmt --to=iec-i --suffix=B "$1" 2>/dev/null || echo "${1}B"
}
# ---------------------------------------------------------------------------
# Phase 1: Check if cleanup is needed
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
log "Disk usage: ${usage}% (threshold: ${THRESHOLD}%)"
if [ "$usage" -lt "$THRESHOLD" ]; then
log "Below threshold, running light cleanup only"
AGGRESSIVE=false
else
log "Above threshold, running aggressive cleanup"
AGGRESSIVE=true
fi
# ---------------------------------------------------------------------------
# Phase 2: Docker cleanup (always runs, safe)
# ---------------------------------------------------------------------------
log "--- Docker cleanup ---"
# Remove stopped containers
stopped=$(docker ps -aq --filter status=exited --filter status=dead 2>/dev/null | wc -l)
if [ "$stopped" -gt 0 ]; then
docker rm $(docker ps -aq --filter status=exited --filter status=dead) 2>/dev/null || true
log "Removed ${stopped} stopped containers"
fi
# Remove dangling images (untagged layers)
dangling=$(docker images -q --filter dangling=true 2>/dev/null | wc -l)
if [ "$dangling" -gt 0 ]; then
docker rmi $(docker images -q --filter dangling=true) 2>/dev/null || true
log "Removed ${dangling} dangling images"
fi
# Prune build cache
docker builder prune -f --filter "until=${DOCKER_IMAGE_MAX_AGE}" 2>/dev/null || true
log "Pruned Docker build cache older than ${DOCKER_IMAGE_MAX_AGE}"
if [ "$AGGRESSIVE" = true ]; then
# Remove all images not used by running containers
running_images=$(docker ps -q 2>/dev/null | xargs -r docker inspect --format='{{.Image}}' | sort -u)
all_images=$(docker images -q 2>/dev/null | sort -u)
for img in $all_images; do
if ! echo "$running_images" | grep -q "$img"; then
docker rmi -f "$img" 2>/dev/null || true
fi
done
log "Removed unused Docker images (aggressive)"
# Prune volumes
docker volume prune -f 2>/dev/null || true
log "Pruned unused Docker volumes"
fi
# ---------------------------------------------------------------------------
# Phase 3: act_runner action cache cleanup
# ---------------------------------------------------------------------------
log "--- Action cache cleanup ---"
if [ -d "$CACHE_DIR" ]; then
before=$(du -sb "$CACHE_DIR" 2>/dev/null | cut -f1)
# Delete cache entries older than max age
find "$CACHE_DIR" -type f -mtime "+${CACHE_MAX_AGE_DAYS}" -delete 2>/dev/null || true
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null || true
after=$(du -sb "$CACHE_DIR" 2>/dev/null | cut -f1)
freed=$((before - after))
log "Cache cleanup freed $(bytes_to_human $freed) (entries older than ${CACHE_MAX_AGE_DAYS}d)"
else
log "Cache directory not found: ${CACHE_DIR}"
# Try common alternative locations
for alt in /var/lib/act_runner/.cache/actcache /home/*/.cache/actcache; do
if [ -d "$alt" ]; then
log "Found cache at: $alt (update CACHE_DIR config)"
CACHE_DIR="$alt"
find "$CACHE_DIR" -type f -mtime "+${CACHE_MAX_AGE_DAYS}" -delete 2>/dev/null || true
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null || true
break
fi
done
fi
# ---------------------------------------------------------------------------
# Phase 4: Workspace cleanup
# ---------------------------------------------------------------------------
log "--- Workspace cleanup ---"
for ws in "${WORKSPACES[@]}"; do
if [ -d "$ws" ]; then
# Remove workspace dirs not modified in the last 2 hours
# (active builds should be touching files continuously)
before=$(du -sb "$ws" 2>/dev/null | cut -f1)
find "$ws" -mindepth 1 -maxdepth 1 -type d -mmin +120 -exec rm -rf {} + 2>/dev/null || true
after=$(du -sb "$ws" 2>/dev/null | cut -f1)
freed=$((before - after))
if [ "$freed" -gt 0 ]; then
log "Workspace $ws: freed $(bytes_to_human $freed)"
fi
fi
done
# ---------------------------------------------------------------------------
# Phase 5: System cleanup
# ---------------------------------------------------------------------------
log "--- System cleanup ---"
# apt cache
apt-get clean 2>/dev/null || true
# Truncate large log files (keep last 1000 lines)
for logfile in /var/log/syslog /var/log/daemon.log /var/log/kern.log; do
if [ -f "$logfile" ] && [ "$(stat -c%s "$logfile" 2>/dev/null)" -gt 104857600 ]; then
tail -1000 "$logfile" > "${logfile}.tmp" && mv "${logfile}.tmp" "$logfile"
log "Truncated $logfile (was >100MB)"
fi
done
# Journal logs older than 3 days
journalctl --vacuum-time=3d 2>/dev/null || true
# ---------------------------------------------------------------------------
# Phase 6: Emergency cleanup (only if still critical)
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
if [ "$usage" -gt 95 ]; then
log "CRITICAL: Still at ${usage}% after cleanup"
# Nuclear option: remove ALL docker data except running containers
docker system prune -af --volumes 2>/dev/null || true
log "Ran docker system prune -af --volumes"
# Clear entire action cache
if [ -d "$CACHE_DIR" ]; then
rm -rf "${CACHE_DIR:?}/"*
log "Cleared entire action cache"
fi
usage=$(disk_usage_pct)
log "After emergency cleanup: ${usage}%"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
log "Cleanup complete. Disk usage: ${usage}%"
# Report top space consumers for diagnostics
log "Top 10 directories under /var:"
du -sh /var/*/ 2>/dev/null | sort -rh | head -10 | while read -r line; do
log " $line"
done

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run CI runner cleanup every 30 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Persistent=true
[Install]
WantedBy=timers.target

146
.gitea/workflows/build.yml Normal file
View File

@@ -0,0 +1,146 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
name: Build and Test
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
CCACHE_DIR: /tmp/ccache-kindred-create
CCACHE_COMPRESS: "true"
CCACHE_COMPRESSLEVEL: "6"
CCACHE_MAXSIZE: "4G"
CCACHE_SLOPPINESS: "include_file_ctime,include_file_mtime,pch_defines,time_macros"
CCACHE_COMPILERCHECK: "content"
CCACHE_BASEDIR: ${{ github.workspace }}
DEBIAN_FRONTEND: noninteractive
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/ca-certificates.crt
steps:
- name: Trust Cloudflare origin CA
run: |
apt-get update -qq
apt-get install -y --no-install-recommends ca-certificates
update-ca-certificates
- name: Free disk space
run: |
echo "=== Disk usage before cleanup ==="
df -h /
rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache 2>/dev/null || true
rm -rf /usr/local/share/boost /usr/share/swift 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
apt-get clean 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Install system prerequisites
run: |
apt-get update -qq
apt-get install -y --no-install-recommends \
ca-certificates curl git xvfb xauth openssl sudo \
libgl1-mesa-dev libglu1-mesa-dev libx11-dev libxkbcommon-dev \
libxcb-xkb-dev libfontconfig1-dev
- name: Checkout repository
uses: https://git.kindred-systems.com/actions/checkout.git@v4
with:
submodules: recursive
fetch-depth: 1
- name: Fetch latest tag (for git describe)
run: |
latest_tag=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | grep -v '\^{}' | head -n1 | awk '{print $2}')
if [ -n "$latest_tag" ]; then
git fetch --no-recurse-submodules --force --depth=1 origin "+${latest_tag}:${latest_tag}"
fi
- name: Install pixi
run: |
curl -fsSL https://pixi.sh/install.sh | bash
echo "$HOME/.pixi/bin" >> $GITHUB_PATH
export PATH="$HOME/.pixi/bin:$PATH"
pixi --version
- name: Run Kindred addon tests (pure logic, no build needed)
timeout-minutes: 2
run: python3 tests/run_kindred_tests.py
- name: Restore ccache
id: ccache-restore
uses: https://git.kindred-systems.com/actions/cache.git/restore@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-build-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
ccache-build-${{ github.ref_name }}-
ccache-build-main-
- name: Prepare ccache
run: |
mkdir -p $CCACHE_DIR
pixi run ccache -z
pixi run ccache -p
- name: Configure (CMake)
run: pixi run cmake --preset conda-linux-release
- name: Build
run: pixi run cmake --build build/release -j$(nproc)
- name: Show ccache statistics
run: pixi run ccache -s
- name: Save ccache
if: always()
uses: https://git.kindred-systems.com/actions/cache.git/save@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-build-${{ github.ref_name }}-${{ github.run_id }}
- name: Run C++ unit tests
continue-on-error: true
timeout-minutes: 15
run: |
export CTEST_DISCOVERY_TIMEOUT=60
pixi run xvfb-run -a ctest --test-dir build/release \
--output-on-failure \
--timeout 120 \
--exclude-regex "Assembly_tests" \
2>&1 || true
- name: Install
run: pixi run cmake --install build/release --prefix build/release/install
- name: Run Python CLI tests
timeout-minutes: 10
run: pixi run timeout 300 build/release/bin/FreeCADCmd -t 0 || true
- name: Run GUI tests (headless)
timeout-minutes: 10
run: pixi run timeout 300 xvfb-run -a build/release/bin/FreeCAD -t 0 || true
- name: Package build artifacts
run: |
ARTIFACT_NAME="kindred-create-$(git describe --tags --always)-linux-x86_64"
cd build/release
cp -a install "${ARTIFACT_NAME}"
tar -cJf "${ARTIFACT_NAME}.tar.xz" "${ARTIFACT_NAME}"
sha256sum "${ARTIFACT_NAME}.tar.xz" > "${ARTIFACT_NAME}.tar.xz.sha256"
echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
- name: Upload build artifact
uses: https://git.kindred-systems.com/actions/upload-artifact.git@v3
with:
name: ${{ env.ARTIFACT_NAME }}
path: |
build/release/*.tar.xz
build/release/*.sha256
retention-days: 14

51
.gitea/workflows/docs.yml Normal file
View File

@@ -0,0 +1,51 @@
name: Deploy Docs
on:
push:
branches: [main]
paths:
- "docs/**"
- ".gitea/workflows/docs.yml"
- ".gitea/workflows/sync-silo-docs.yml"
jobs:
build-and-deploy:
runs-on: docs
steps:
- name: Checkout
run: |
REPO_URL="http://gitea:3000/kindred/create.git"
if [ -d .git ]; then
git fetch "$REPO_URL" main
git checkout -f FETCH_HEAD
else
git clone --depth 1 --branch main "$REPO_URL" .
fi
- name: Install mdBook
run: |
if ! command -v mdbook &>/dev/null; then
MDBOOK_VERSION="v0.5.2"
wget -q -O mdbook.tar.gz "https://github.com/rust-lang/mdBook/releases/download/${MDBOOK_VERSION}/mdbook-${MDBOOK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
tar -xzf mdbook.tar.gz -C /usr/local/bin
rm mdbook.tar.gz
fi
- name: Fetch Silo server docs
run: |
rm -rf /tmp/silo
git clone --depth 1 http://gitea:3000/kindred/silo.git /tmp/silo
mkdir -p docs/src/silo-server
cp /tmp/silo/docs/*.md docs/src/silo-server/
cp /tmp/silo/README.md docs/src/silo-server/overview.md
cp /tmp/silo/ROADMAP.md docs/src/silo-server/ROADMAP.md 2>/dev/null || true
cp /tmp/silo/frontend-spec.md docs/src/silo-server/frontend-spec.md 2>/dev/null || true
rm -rf /tmp/silo
- name: Build mdBook
run: mdbook build docs/
- name: Deploy
run: |
rm -rf /opt/git/docs/book/*
cp -r docs/book/* /opt/git/docs/book/

View File

@@ -0,0 +1,438 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
name: Release Build
on:
push:
tags: ["v*", "latest"]
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g., v0.1.0)"
required: true
type: string
jobs:
# ---------------------------------------------------------------------------
# Linux: AppImage + .deb
# ---------------------------------------------------------------------------
build-linux:
runs-on: ubuntu-latest
env:
CCACHE_DIR: /tmp/ccache-kindred-create
CCACHE_COMPRESS: "true"
CCACHE_COMPRESSLEVEL: "6"
CCACHE_MAXSIZE: "4G"
CCACHE_SLOPPINESS: "include_file_ctime,include_file_mtime,pch_defines,time_macros"
CCACHE_COMPILERCHECK: "content"
CCACHE_BASEDIR: ${{ github.workspace }}
BUILD_TAG: ${{ github.ref_name || inputs.tag }}
CFLAGS: "-O3"
CXXFLAGS: "-O3"
DEBIAN_FRONTEND: noninteractive
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/ca-certificates.crt
steps:
- name: Trust Cloudflare origin CA
run: |
apt-get update -qq
apt-get install -y --no-install-recommends ca-certificates
update-ca-certificates
- name: Free disk space
run: |
echo "=== Disk usage before cleanup ==="
df -h /
# Remove pre-installed bloat common in runner images
rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache 2>/dev/null || true
rm -rf /usr/local/share/boost /usr/share/swift 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
apt-get clean 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Install system prerequisites
run: |
apt-get update -qq
apt-get install -y --no-install-recommends \
ca-certificates curl git file fuse3 xvfb xauth openssl sudo dpkg-dev \
libgl1-mesa-dev libglu1-mesa-dev libx11-dev libxkbcommon-dev \
libxcb-xkb-dev libfontconfig1-dev
- name: Checkout repository
uses: https://git.kindred-systems.com/actions/checkout.git@v4
with:
submodules: recursive
fetch-depth: 1
- name: Fetch latest tag (for git describe)
run: |
latest_tag=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | grep -v '\^{}' | head -n1 | awk '{print $2}')
if [ -n "$latest_tag" ]; then
git fetch --no-recurse-submodules --force --depth=1 origin "+${latest_tag}:${latest_tag}"
fi
- name: Install pixi
run: |
curl -fsSL https://pixi.sh/install.sh | bash
echo "$HOME/.pixi/bin" >> $GITHUB_PATH
export PATH="$HOME/.pixi/bin:$PATH"
pixi --version
- name: Restore ccache
id: ccache-restore
uses: https://git.kindred-systems.com/actions/cache.git/restore@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-release-linux-${{ github.run_id }}
restore-keys: |
ccache-release-linux-
ccache-build-main-
- name: Prepare ccache
run: |
mkdir -p $CCACHE_DIR
# Ensure ccache is accessible to rattler-build's subprocess
export PATH="$(pixi run bash -c 'echo $PATH')"
pixi run ccache -z
pixi run ccache -p
- name: Build release package (AppImage)
working-directory: package/rattler-build
run: |
pixi install
pixi run -e package create_bundle
- name: Show ccache statistics
run: pixi run ccache -s
- name: Save ccache
if: always()
uses: https://git.kindred-systems.com/actions/cache.git/save@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-release-linux-${{ github.run_id }}
- name: Clean up intermediate build files
run: |
# Remove pixi package cache and build work dirs to free space for .deb
rm -rf package/rattler-build/.pixi/build 2>/dev/null || true
find /root/.cache/rattler -type f -delete 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Build .deb package
run: |
./package/debian/build-deb.sh \
package/rattler-build/linux/AppDir/usr \
package/rattler-build/linux \
"${BUILD_TAG}"
- name: List built artifacts
run: |
echo "=== Linux release artifacts ==="
ls -lah package/rattler-build/linux/*.AppImage* 2>/dev/null || true
ls -lah package/rattler-build/linux/*.deb* 2>/dev/null || true
ls -lah package/rattler-build/linux/*-SHA256.txt 2>/dev/null || true
- name: Upload Linux artifacts
uses: https://git.kindred-systems.com/actions/upload-artifact.git@v3
with:
name: release-linux
path: |
package/rattler-build/linux/FreeCAD_*.AppImage
package/rattler-build/linux/*.deb
package/rattler-build/linux/*-SHA256.txt
package/rattler-build/linux/*.sha256
if-no-files-found: error
# ---------------------------------------------------------------------------
# macOS: DMG (Intel + Apple Silicon)
# TODO: Re-enable when macOS runners are available or cross-compilation is set up
# ---------------------------------------------------------------------------
# build-macos:
# strategy:
# fail-fast: false
# matrix:
# include:
# - runner: macos-13
# arch: x86_64
# - runner: macos-14
# arch: arm64
#
# runs-on: ${{ matrix.runner }}
#
# env:
# CCACHE_DIR: /tmp/ccache-kindred-create
# CCACHE_COMPRESS: "true"
# CCACHE_COMPRESSLEVEL: "6"
# CCACHE_MAXSIZE: "4G"
# CCACHE_SLOPPINESS: "include_file_ctime,include_file_mtime,pch_defines,time_macros"
# CCACHE_BASEDIR: ${{ github.workspace }}
# BUILD_TAG: ${{ github.ref_name || inputs.tag }}
# CFLAGS: "-O3"
# CXXFLAGS: "-O3"
#
# steps:
# - name: Checkout repository
# uses: https://git.kindred-systems.com/actions/checkout.git@v4
# with:
# submodules: recursive
# fetch-depth: 1
#
# - name: Fetch tags
# run: git fetch --tags --force --no-recurse-submodules origin
#
# - name: Install pixi
# run: |
# curl -fsSL https://pixi.sh/install.sh | bash
# echo "$HOME/.pixi/bin" >> $GITHUB_PATH
# export PATH="$HOME/.pixi/bin:$PATH"
# pixi --version
#
# - name: Restore ccache
# id: ccache-restore
# uses: https://git.kindred-systems.com/actions/cache.git/restore@v4
# with:
# path: /tmp/ccache-kindred-create
# key: ccache-release-macos-${{ matrix.arch }}-${{ github.sha }}
# restore-keys: |
# ccache-release-macos-${{ matrix.arch }}-
#
# - name: Prepare ccache
# run: |
# mkdir -p $CCACHE_DIR
# pixi run ccache -z
#
# - name: Build release package (DMG)
# working-directory: package/rattler-build
# run: |
# pixi install
# pixi run -e package create_bundle
#
# - name: Show ccache statistics
# run: pixi run ccache -s
#
# - name: Save ccache
# if: always()
# uses: https://git.kindred-systems.com/actions/cache.git/save@v4
# with:
# path: /tmp/ccache-kindred-create
# key: ccache-release-macos-${{ matrix.arch }}-${{ github.sha }}
#
# - name: List built artifacts
# run: |
# echo "=== macOS ${{ matrix.arch }} release artifacts ==="
# ls -lah package/rattler-build/osx/*.dmg* 2>/dev/null || true
#
# - name: Upload macOS artifacts
# uses: https://git.kindred-systems.com/actions/upload-artifact.git@v3
# with:
# name: release-macos-${{ matrix.arch }}
# path: |
# package/rattler-build/osx/*.dmg
# package/rattler-build/osx/*-SHA256.txt
# if-no-files-found: error
# ---------------------------------------------------------------------------
# Windows: .exe installer + .7z archive
# TODO: Re-enable when Windows runners are available or cross-compilation is set up
# ---------------------------------------------------------------------------
# build-windows:
# runs-on: windows-latest
#
# env:
# CCACHE_DIR: C:\ccache-kindred-create
# CCACHE_COMPRESS: "true"
# CCACHE_COMPRESSLEVEL: "6"
# CCACHE_MAXSIZE: "4G"
# CCACHE_SLOPPINESS: "include_file_ctime,include_file_mtime,pch_defines,time_macros"
# CCACHE_BASEDIR: ${{ github.workspace }}
# BUILD_TAG: ${{ github.ref_name || inputs.tag }}
# CFLAGS: "/O2"
# CXXFLAGS: "/O2"
# MAKE_INSTALLER: "true"
#
# steps:
# - name: Checkout repository
# uses: https://git.kindred-systems.com/actions/checkout.git@v4
# with:
# submodules: recursive
# fetch-depth: 1
#
# - name: Fetch tags
# shell: bash
# run: git fetch --tags --force --no-recurse-submodules origin
#
# - name: Install pixi
# shell: bash
# run: |
# curl -fsSL https://pixi.sh/install.sh | bash
# echo "$HOME/.pixi/bin" >> $GITHUB_PATH
# export PATH="$HOME/.pixi/bin:$PATH"
# pixi --version
#
# - name: Restore ccache
# id: ccache-restore
# uses: https://git.kindred-systems.com/actions/cache.git/restore@v4
# with:
# path: C:\ccache-kindred-create
# key: ccache-release-windows-${{ github.sha }}
# restore-keys: |
# ccache-release-windows-
#
# - name: Build release package
# shell: bash
# working-directory: package/rattler-build
# run: |
# pixi install
# pixi run -e package create_bundle
#
# - name: Save ccache
# if: always()
# uses: https://git.kindred-systems.com/actions/cache.git/save@v4
# with:
# path: C:\ccache-kindred-create
# key: ccache-release-windows-${{ github.sha }}
#
# - name: List built artifacts
# shell: bash
# run: |
# echo "=== Windows release artifacts ==="
# ls -lah package/rattler-build/windows/*.7z* 2>/dev/null || true
# ls -lah package/rattler-build/windows/*.exe 2>/dev/null || true
# ls -lah package/rattler-build/windows/*-SHA256.txt 2>/dev/null || true
#
# - name: Upload Windows artifacts
# uses: https://git.kindred-systems.com/actions/upload-artifact.git@v3
# with:
# name: release-windows
# path: |
# package/rattler-build/windows/*.7z
# package/rattler-build/windows/*.exe
# package/rattler-build/windows/*-SHA256.txt
# if-no-files-found: error
# ---------------------------------------------------------------------------
# Create Gitea release from all platform artifacts
# ---------------------------------------------------------------------------
publish-release:
needs: [build-linux] # TODO: Add build-macos, build-windows when runners are available
runs-on: ubuntu-latest
env:
BUILD_TAG: ${{ github.ref_name || inputs.tag }}
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/ca-certificates.crt
steps:
- name: Trust Cloudflare origin CA
run: |
apt-get update -qq
apt-get install -y --no-install-recommends ca-certificates
update-ca-certificates
- name: Download all artifacts
uses: https://git.kindred-systems.com/actions/download-artifact.git@v3
with:
path: artifacts
- name: List all release artifacts
run: |
echo "=== All release artifacts ==="
find artifacts -type f | sort
- name: Collect release files
run: |
mkdir -p release
find artifacts -type f \( \
-name "*.AppImage" -o \
-name "*.deb" -o \
-name "*.dmg" -o \
-name "*.7z" -o \
-name "*.exe" -o \
-name "*SHA256*" -o \
-name "*.sha256" \
\) -exec cp {} release/ \;
echo "=== Release files ==="
ls -lah release/
- name: Create release
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: |
TAG="${BUILD_TAG}"
# Build JSON payload entirely in Python to avoid shell/Python type mismatches
PAYLOAD=$(python3 -c "
import json, re
tag = '${TAG}'
prerelease = bool(re.search(r'(rc|beta|alpha)', tag))
body = '''## Kindred Create {tag}
### Downloads
| Platform | File |
|----------|------|
| Linux (AppImage) | \`KindredCreate-*-Linux-x86_64.AppImage\` |
| Linux (Debian/Ubuntu) | \`kindred-create_*.deb\` |
*macOS and Windows builds are not yet available.*
SHA256 checksums are provided alongside each artifact.'''.format(tag=tag)
print(json.dumps({
'tag_name': tag,
'name': f'Kindred Create {tag}',
'body': body,
'prerelease': prerelease,
}))
")
# Delete existing release for this tag (if any) so we can recreate
existing=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}")
if [ "$existing" = "200" ]; then
release_id=$(curl -s \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${release_id}"
echo "Deleted existing release ${release_id} for tag ${TAG}"
fi
# Create release
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases")
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::error::Failed to create release (HTTP ${http_code}): ${body}"
exit 1
fi
release_id=$(echo "$body" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Created release ${release_id}"
# Upload assets
for file in release/*; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
upload_resp=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${file}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${release_id}/assets?name=${filename}")
upload_code=$(echo "$upload_resp" | tail -1)
if [ "$upload_code" -lt 200 ] || [ "$upload_code" -ge 300 ]; then
echo "::warning::Failed to upload ${filename} (HTTP ${upload_code})"
else
echo " done."
fi
done

View File

@@ -0,0 +1,53 @@
name: Sync Silo Server Docs
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * *" # Daily at 06:00 UTC as fallback
jobs:
sync:
runs-on: docs
steps:
- name: Checkout create repo
run: |
REPO_URL="http://gitea:3000/kindred/create.git"
if [ -d .git ]; then
git fetch "$REPO_URL" main
git checkout -f FETCH_HEAD
else
git clone --depth 1 --branch main "$REPO_URL" .
fi
- name: Clone Silo server docs
run: |
rm -rf /tmp/silo
git clone --depth 1 http://gitea:3000/kindred/silo.git /tmp/silo
mkdir -p docs/src/silo-server
cp /tmp/silo/docs/*.md docs/src/silo-server/
cp /tmp/silo/README.md docs/src/silo-server/overview.md
cp /tmp/silo/ROADMAP.md docs/src/silo-server/ROADMAP.md 2>/dev/null || true
cp /tmp/silo/frontend-spec.md docs/src/silo-server/frontend-spec.md 2>/dev/null || true
rm -rf /tmp/silo
- name: Check for changes
id: diff
run: |
git add docs/src/silo-server/
if git diff --cached --quiet; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Commit and push
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "Kindred Bot"
git config user.email "bot@kindred-systems.com"
git commit -m "docs: sync Silo server documentation
Auto-synced from kindred/silo main branch."
PUSH_URL="http://kindred-bot:${{ secrets.RELEASE_TOKEN }}@gitea:3000/kindred/create.git"
git push "$PUSH_URL" HEAD:main

8
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "src/3rdParty/OndselSolver"]
path = src/3rdParty/OndselSolver
url = https://github.com/FreeCAD/OndselSolver.git
url = https://git.kindred-systems.com/kindred/solver.git
[submodule "tests/lib"]
path = tests/lib
url = https://github.com/google/googletest
@@ -10,3 +10,9 @@
[submodule "src/Mod/AddonManager"]
path = src/Mod/AddonManager
url = https://github.com/FreeCAD/AddonManager.git
[submodule "mods/ztools"]
path = mods/ztools
url = https://git.kindred-systems.com/forbes/ztools.git
[submodule "mods/silo"]
path = mods/silo
url = https://git.kindred-systems.com/kindred/silo-mod.git

View File

@@ -1,111 +1,131 @@
# FreeCAD Contribution Process (FCP)
# Contributing to Kindred Create
FreeCAD's contribution process is inspired by the Collective Code Construction Contract which itself is an evolution of the github.com Fork and Pull Model.
Kindred Create is maintained at [git.kindred-systems.com/kindred/create](https://git.kindred-systems.com/kindred/create). Contributions are submitted as pull requests against the `main` branch.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
## Getting started
```bash
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
cd create
pixi run configure
pixi run build
pixi run freecad
```
## 0. Status
See the [README](README.md) for full build instructions.
FreeCAD is in a transition period. The following are to be regarded as GUIDELINES for contribution submission and acceptance. For historical reasons, the actual process MAY diverge from this process during the transition. Such deviations SHOULD be noted and discussed whenever possible.
## Branch and PR workflow
## 1. Goals
1. Create a feature branch from `main`:
```bash
git checkout -b feat/my-feature main
```
2. Make your changes, commit with conventional commit messages (see below).
3. Push and open a pull request against `main`.
4. CI builds and tests run automatically on all PRs.
The FreeCAD Contribution Process is expressed here with the following specific goals in mind:
## Commit messages
1. To provide transparency and fairness in the contribution process.
2. To allow contributions to be included as quickly as possible.
3. To preserve and improve the code quality while encouraging appropriate experimentation and risk-taking.
4. To minimize dependence on individual Contributors by encouraging a large pool of active Contributors.
5. To be inclusive of many viewpoints and to harness a diverse set of skills.
6. To provide an encouraging environment where Contributors learn and improve their skills.
7. To protect the free and open nature of the FreeCAD project.
Use [Conventional Commits](https://www.conventionalcommits.org/):
## 2. Fundamentals
| Prefix | Purpose |
|--------|---------|
| `feat:` | New feature |
| `fix:` | Bug fix |
| `chore:` | Maintenance, dependencies |
| `docs:` | Documentation only |
| `art:` | Icons, theme, visual assets |
1. FreeCAD uses the git distributed revision control system.
2. Source code for the main application and related subprojects is hosted on github.com in the FreeCAD organization.
3. Problems are discrete, well-defined limitations or bugs.
4. FreeCAD uses GitHub's issue-tracking system to track problems and contributions. For help requests and general discussions, use the project forum.
5. Contributions are sets of code changes that resolve a single problem.
6. FreeCAD uses the Pull Request workflow for evaluating and accepting contributions.
Examples:
- `feat: add datum point creation mode`
- `fix(gui): correct menu icon size on Wayland`
- `chore: update silo submodule`
## 3. Roles
1. "User": A member of the wider FreeCAD community who uses the software.
2. "Contributor": A person who submits a contribution that resolves a previously identified problem. Contributors do not have commit access to the repository unless they are also Maintainers. Everyone, without distinction or discrimination, SHALL have an equal right to become a Contributor.
3. "Maintainer": A person who merges contributions. Maintainers may or may not be Contributors. Their role is to enforce the process. Maintainers have commit access to the repository.
4. "Administrator": Administrators have additional authority to maintain the list of designated Maintainers.
## Code style
## 4. Licensing, Ownership, and Credit
1. FreeCAD is distributed under the Lesser General Public License, version 2, or superior (LGPL2+). Additional details can be found in the LICENSE file.
2. All contributions to FreeCAD MUST use a compatible license.
3. All contributions are owned by their authors unless assigned to another.
4. FreeCAD does not have a mandatory copyright assignment policy.
5. A Contributor who wishes to be identified in the Credits section of the application "About" dialog is responsible for identifying themselves. They should modify the Contributors file and submit a PR with a single commit for this modification only. The contributors file is found at https://github.com/FreeCAD/FreeCAD/blob/main/src/Doc/CONTRIBUTORS
6. A contributor who does not wish to assume the copyright of their contribution MAY choose to assign it to the [FreeCAD project association](https://fpa.freecad.org) by mentioning **Copyright (c) 2022 The FreeCAD project association <fpa@freecad.org>** in the file's license code block.
### C/C++
## 5. Contribution Requirements
Formatted with **clang-format** (config in `.clang-format`). Static analysis via **clang-tidy** (config in `.clang-tidy`).
1. Contributions are submitted in the form of Pull Requests (PR).
2. Maintainers and Contributors MUST have a GitHub account and SHOULD use their real names or a well-known alias.
3. If the GitHub username differs from the username on the FreeCAD Forum, effort SHOULD be taken to avoid confusion.
4. A PR SHOULD be a minimal and accurate answer to exactly one identified and agreed-on problem.
5. A PR SHOULD refrain from adding additional dependencies to the FreeCAD project unless no other option is available.
6. Code submissions MUST adhere to the code style guidelines of the project if these are defined.
7. If a PR contains multiple commits, each commit MUST compile cleanly when merged with all previous commits of the same PR. Each commit SHOULD add value to the history of the project. Checkpoint commits SHOULD be squashed.
8. A PR SHALL NOT include non-trivial code from other projects unless the Contributor is the original author of that code.
9. A PR MUST compile cleanly and pass project self-tests on all target platforms.
10. Changes that break python API used by extensions SHALL be avoided. If it is not possible to avoid breaking changes, the amount of them MUST be minimized and PR MUST clearly describe all breaking changes with clear description on how to replace no longer working solution with newer one. Contributor SHOULD search for addons that will be broken and list them in the PR.
11. Each commit message in a PR MUST succinctly explain what the commit achieves. The commit message SHALL follow the suggestions in the `git commit --help` documentation, section DISCUSSION.
12. The PR Title MUST succinctly explain what the PR achieves. The Body MAY be as detailed as needed. If a PR changes the user interface (UI), the body of the text MUST include a presentation of these UI changes, preferably with screenshots of the previous and revised state.
13. If a PR contains the work of another author (for example, if it is cherry-picked from a fork by someone other than the PR-submitter):
1. the PR description MUST contain proper attribution as the first line, for example: "This is work of XYZ cherry-picked from <link>";
2. all commits MUST have proper authorship, i.e. be authored by the original author and committed by the author of the PR;
3. if changes to cherry-picked commits are necessary they SHOULD be done as follow-up commits. If it is not possible to do so, then the modified commits MUST contain a `Co-Authored-By` trailer in their commit message.
14. A “Valid PR” is one which satisfies the above requirements.
### Python
## 6. Process
Formatted with **black** (100-character line length). Linted with **pylint** (config in `.pylintrc`).
1. Change on the project follows the pattern of accurately identifying problems and applying minimal, accurate solutions to these problems.
2. To request changes, a User logs an issue on the project GitHub issue tracker.
3. The User or Contributor SHOULD write the issue by describing the problem they face or observe. Links to the forum or other resources are permitted but the issue SHOULD be complete and accurate and SHOULD NOT require the reader to visit the forum or any other platform to understand what is being described.
4. Issue authors SHOULD strive to describe the minimum acceptable condition.
5. Issue authors SHOULD focus on User tasks and avoid comparisons to other software solutions.
6. The User or Contributor SHOULD seek consensus on the accuracy of their observation and the value of solving the problem.
7. To submit a solution to a problem, a Contributor SHALL create a pull request back to the project.
8. Contributors and Maintainers SHALL NOT commit changes directly to the target branch.
9. To discuss a proposed solution, Users MAY comment on the Pull Request in GitHub. Forum conversations regarding the solution SHOULD be discouraged and conversation redirected to the Pull Request or the related issue.
10. To accept or reject a Pull Request, a Maintainer SHALL use GitHub's interface.
11. Maintainers SHOULD NOT merge their own PRs except:
1. in exceptional cases, such as non-responsiveness from other Maintainers for an extended period.
2. If the Maintainer is also the primary developer of the workbench or subsystem.
### Pre-commit hooks
12. Maintainers SHALL merge valid PRs from other Contributors rapidly.
13. Maintainers MAY, at their discretion merge PRs that have not met all criteria to be considered valid to:
1. end fruitless discussions
2. capture toxic contributions in the historical record
3. engage with the Contributor on improving their contribution quality.
14. Maintainers SHALL NOT make value judgments on correct contributions.
15. If a PR requires significant further work before merging, the PR SHOULD be moved to draft status.
16. If a PR is complete, but should not be merged yet (for example, because it depends on another in-process PR), the "On hold" label SHOULD be applied.
17. Any Contributor who has value judgments on a PR SHOULD express these via their own PR.
18. The User who created an issue SHOULD close the issue after checking the PR is successful.
19. Maintainers SHOULD close issues that are left open without action or update for an unreasonable period.
```bash
pip install pre-commit
pre-commit install
```
## 7. Branches and Releases
This runs clang-format, black, and pylint automatically on staged files.
1. The project SHALL have one branch (“main”) that always holds the latest in-progress version and SHOULD always build.
2. The project SHALL NOT use topic branches for any reason. Personal forks MAY use topic branches.
3. To make a stable release a Maintainer SHALL tag the repository. Stable releases SHALL always be released from the repository main branch.
## Submodules
## 8. Project Administration
Kindred Create uses git submodules for addon workbenches:
1. Project Administrators are those individuals who are members of the FreeCAD Github organization and have the role of 'owner'. They have the task of administering the organization including adding and removing individuals from various teams.
2. Project Administrator is a technical role necessitated by the GitHub platform. Except for the specific exceptions listed below, the Project Administrators do not make the decision about individual team members. Rather, they carry out the collective wishes of the Maintainers team. Project Administrators will be selected from the Maintainers team by the Maintainers themselves.
3. To ensure continuity there SHALL be at least four Project Administrators at all times.
4. The project Administrators will manage the set of project Maintainers. They SHALL maintain a sufficiently large pool of Maintainers to ensure their succession and permit timely review of contributions. If the pool of Maintainers is insufficient, the Project Administrators will request that the Maintainers select additional individuals to add.
5. Contributors who have a history of successful PRs and have demonstrated continued professionalism should be invited to be Maintainers.
6. Administrators SHOULD remove Maintainers who are inactive for an extended period, or who repeatedly fail to apply this process accurately.
7. The list of Maintainers SHALL be publicly accessible and reflective of current activity on the project.
8. Administrators SHALL act expediently to protect the FreeCAD infrastructure and resources.
9. Administrators SHOULD block or ban “bad actors” who cause stress, animosity, or confusion to others in the project. This SHOULD be done after public discussion, with a chance for all parties to speak. A bad actor is someone who repeatedly ignores the rules and culture of the project, who is hostile or offensive, who impedes the productive exchange of information, and who is unable to self-correct their behavior when asked to do so by others.
| Submodule | Path | Repository |
|-----------|------|------------|
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` |
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` |
To update a submodule:
```bash
cd mods/silo
git checkout main && git pull
cd ../..
git add mods/silo
git commit -m "chore: update silo submodule"
```
If you cloned without `--recursive`, initialize submodules with:
```bash
git submodule update --init --recursive
```
## Theme and QSS changes
The Catppuccin Mocha theme has **three QSS copies** that must be kept in sync:
1. `resources/preferences/KindredCreate/KindredCreate.qss` (canonical)
2. `src/Gui/Stylesheets/KindredCreate.qss`
3. `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`
When modifying the theme, apply changes to all three files. Note that the copies have intentional differences (e.g., tree branch rendering style), so do not blindly copy between them -- apply edits individually.
See [KNOWN_ISSUES.md](docs/KNOWN_ISSUES.md) for the planned QSS consolidation.
## Preference pack
Default preferences are defined in `resources/preferences/KindredCreate/KindredCreate.cfg`. This XML file uses FreeCAD's parameter format:
```xml
<FCParamGroup Name="GroupName">
<FCBool Name="Setting" Value="1"/>
<FCInt Name="Setting" Value="42"/>
<FCText Name="Setting">value</FCText>
</FCParamGroup>
```
Changes here affect the out-of-box experience for all users.
## CI/CD
- **Build workflow** (`build.yml`): Runs on every push to `main` and on PRs. Builds in Ubuntu 24.04 container, runs C++ and Python tests.
- **Release workflow** (`release.yml`): Triggered by `v*` tags. Builds AppImage and .deb packages.
See [docs/CI_CD.md](docs/CI_CD.md) for full details.
## Architecture
For an overview of the codebase structure, bootstrap flow, and design decisions, see:
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) -- Bootstrap flow and source layout
- [docs/COMPONENTS.md](docs/COMPONENTS.md) -- Feature inventory
- [docs/INTEGRATION_PLAN.md](docs/INTEGRATION_PLAN.md) -- Architecture layers and roadmap
## License
All contributions must be compatible with [LGPL-2.1-or-later](LICENSE).

BIN
banner-logo-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

72
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,72 @@
# Architecture
## Bootstrap flow
```
FreeCAD startup
└─ src/Mod/Create/Init.py
└─ setup_kindred_addons()
├─ exec(mods/ztools/ztools/Init.py)
└─ exec(mods/silo/freecad/Init.py)
└─ src/Mod/Create/InitGui.py
├─ setup_kindred_workbenches()
│ ├─ exec(mods/ztools/ztools/InitGui.py)
│ │ └─ schedules deferred _register() (2000ms)
│ │ ├─ imports ZTools commands
│ │ ├─ installs _ZToolsManipulator (global)
│ │ └─ injects commands into editing contexts
│ └─ exec(mods/silo/freecad/InitGui.py)
│ ├─ registers SiloWorkbench
│ └─ schedules deferred Silo overlay registration (2500ms)
├─ EditingContextResolver singleton created (MainWindow constructor)
│ ├─ registers built-in contexts (PartDesign, Sketcher, Assembly, Spreadsheet)
│ ├─ connects to signalInEdit/signalResetEdit/signalActiveDocument/signalActivateView
│ └─ BreadcrumbToolBar connected to contextChanged signal
└─ Deferred setup (QTimer):
├─ 1500ms: _register_silo_origin() → registers Silo FileOrigin
├─ 2000ms: _setup_silo_auth_panel() → "Database Auth" dock
├─ 2000ms: ZTools _register() → commands + manipulator
├─ 2500ms: Silo overlay registration → "Silo Origin" toolbar overlay
├─ 3000ms: _check_silo_first_start() → settings prompt
├─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock (SSE)
└─ 10000ms: _check_for_updates() → update checker (Gitea API)
```
## Key source layout
```
src/Mod/Create/ Kindred bootstrap module (Python)
├── Init.py Adds mods/ addon paths, loads Init.py files
├── InitGui.py Loads workbenches, installs Silo manipulators
├── version.py.in CMake template → version.py (build-time)
└── update_checker.py Checks Gitea releases API for updates
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
src/Gui/CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
src/Gui/OriginManager.h/.cpp Origin lifecycle management
src/Gui/OriginSelectorWidget.h/.cpp UI for origin selection
mods/ztools/ [submodule] command provider (not a workbench)
├── ztools/InitGui.py Deferred command registration + _ZToolsManipulator
├── ztools/ztools/
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
│ ├── datums/core.py Datum creation via Part::AttachExtension
│ └── resources/ Icons, theme utilities
└── CatppuccinMocha/ Theme preference pack (QSS)
mods/silo/ [submodule -> silo-mod.git] FreeCAD workbench
├── silo-client/ [submodule -> silo-client.git] shared API client
│ └── silo_client/ SiloClient, SiloSettings, CATEGORY_NAMES
└── freecad/ FreeCAD workbench (Python)
├── InitGui.py SiloWorkbench + Silo overlay context registration
├── silo_commands.py Commands + FreeCADSiloSettings adapter
└── silo_origin.py FileOrigin backend for Silo
src/Gui/Stylesheets/ QSS themes and SVG assets
src/Gui/PreferencePacks/ KindredCreate preference pack (cfg + build-time QSS)
```
See [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) for architecture layers and phase status.

308
docs/CI_CD.md Normal file
View File

@@ -0,0 +1,308 @@
# CI/CD
Kindred Create uses Gitea Actions for continuous integration and release builds. Workflows are defined in `.gitea/workflows/`.
## Overview
| Workflow | Trigger | Purpose | Artifacts |
|----------|---------|---------|-----------|
| `build.yml` | Push to `main`, pull requests | Build + test | Linux tarball |
| `release.yml` | Tags matching `v*` or `latest` | Release build | AppImage, .deb |
All builds run on public runners in dockerized mode. No host-mode or internal infrastructure is required.
---
## Build workflow (`build.yml`)
Runs on every push to `main` and on pull requests. Builds the project in an Ubuntu 24.04 container and runs the test suite.
### Steps
1. Install system prerequisites (GL, X11, fontconfig headers)
2. Checkout with recursive submodules
3. Install pixi
4. Restore ccache from prior builds
5. Configure via `pixi run cmake --preset conda-linux-release`
6. Build with `pixi run cmake --build build/release -j$(nproc)`
7. Run C++ unit tests under xvfb (Assembly_tests excluded due to discovery timeouts)
8. Install to `build/release/install`
9. Run Python CLI tests (`FreeCADCmd -t 0`)
10. Run GUI tests headless (`xvfb-run FreeCAD -t 0`)
11. Package as `.tar.xz` with SHA256 checksum
12. Upload artifact (14-day retention)
### Caching
ccache is persisted between builds using `actions/cache`. Cache keys use a date suffix so entries rotate daily (one save per day per branch). Saves are skipped when the exact key already exists, preventing duplicate entries that fill runner storage.
```
Key: ccache-build-{branch}-{YYYYMMDD}
Fallback: ccache-build-{branch}-
Fallback: ccache-build-main-
```
Release builds use a separate key namespace (`ccache-release-linux-{YYYYMMDD}`) because they compile with different optimization flags (`-O3`). The rattler-build script (`build.sh`) explicitly sets `CCACHE_DIR` and `CCACHE_BASEDIR` since rattler-build does not forward environment variables from the parent process.
ccache configuration: 4 GB max, zlib compression level 6, sloppy mode for include timestamps and PCH.
---
## Release workflow (`release.yml`)
Triggered by pushing a tag matching `v*` (e.g., `v0.1.0`, `v1.0.0-rc1`). Builds release packages for all platforms in parallel, then creates a Gitea release with all artifacts.
### Triggering a release
```bash
git tag v0.1.0
git push origin v0.1.0
```
Or manually via `workflow_dispatch` with a tag input.
Tags containing `rc`, `beta`, or `alpha` are marked as pre-releases.
### Platform matrix
| Job | Runner | Container | Preset | Output | Status |
|-----|--------|-----------|--------|--------|--------|
| `build-linux` | `ubuntu-latest` | `ubuntu:24.04` | `conda-linux-release` | AppImage, .deb | Active |
| `build-macos` (Intel) | `macos-13` | native | `conda-macos-release` | .dmg (x86_64) | Disabled |
| `build-macos` (Apple Silicon) | `macos-14` | native | `conda-macos-release` | .dmg (arm64) | Disabled |
| `build-windows` | `windows-latest` | native | `conda-windows-release` | .exe (NSIS), .7z | Disabled |
Only the Linux build is currently active. macOS and Windows jobs are defined but commented out pending runner availability or cross-compilation support. After `build-linux` succeeds, `publish-release` collects artifacts and creates the Gitea release.
### Linux build
Both workflows start with a disk cleanup step that removes pre-installed bloat (dotnet, Android SDK, etc.) to free space for the build.
Uses the rattler-build packaging pipeline:
1. `pixi install` in `package/rattler-build/`
2. `pixi run -e package create_bundle` -- invokes `linux/create_bundle.sh`
3. The bundle script:
- Copies the pixi conda environment to an AppDir
- Strips unnecessary files (includes, static libs, cmake files, `__pycache__`)
- Downloads `appimagetool`, extracts it with `--appimage-extract` (FUSE unavailable in containers), and runs via `squashfs-root/AppRun`
- Creates a squashfs AppImage (zstd compressed) with SHA256 checksums
4. Intermediate build files are cleaned up to free space for the .deb step
5. `package/debian/build-deb.sh` builds a .deb from the AppDir
- Installs to `/opt/kindred-create/` with wrapper scripts in `/usr/bin/`
- Sets up LD_LIBRARY_PATH, QT_PLUGIN_PATH, PYTHONPATH in wrappers
- Creates desktop entry, MIME types, AppStream metainfo
### macOS build
Builds natively on macOS runners (Intel via `macos-13`, Apple Silicon via `macos-14`):
1. `pixi run -e package create_bundle` invokes `osx/create_bundle.sh`
2. The bundle script:
- Creates `FreeCAD.app` bundle with conda environment in `Contents/Resources/`
- Runs `fix_macos_lib_paths.py` to convert absolute rpaths to `@loader_path`
- Builds native macOS launcher from `launcher/CMakeLists.txt`
- Patches `Info.plist` with version info
- Creates DMG via `dmgbuild`
- If `SIGN_RELEASE=true`: signs and notarizes via `macos_sign_and_notarize.zsh`
### Windows build
Builds natively on Windows runner:
1. `pixi run -e package create_bundle` invokes `windows/create_bundle.sh` (bash via Git for Windows)
2. The bundle script:
- Copies conda environment DLLs, Python, and FreeCAD binaries to `FreeCAD_Windows/`
- Applies SSL certificate patch (`ssl-patch.py`)
- Creates `qt6.conf` for Qt6 plugin paths
- Creates `.exe` wrapper shims via Chocolatey `shimgen`
- Compresses to `.7z` (compression level 9)
- If `MAKE_INSTALLER=true` and NSIS available: builds NSIS installer
3. NSIS installer (`package/WindowsInstaller/`):
- Multi-language support (28 languages)
- File associations for `.FCStd`
- Start menu and desktop shortcuts
- LZMA compression
### Publish step
`publish-release` runs after all platform builds succeed:
1. Downloads all artifacts from completed build jobs
2. Collects release files (AppImage, .deb, checksums) into a `release/` directory
3. Deletes any existing Gitea release for the same tag (allows re-running)
4. Creates a new Gitea release via the REST API (`/api/v1/repos/{owner}/{repo}/releases`)
5. Uploads each artifact as a release attachment via the API
The release payload (tag name, body, prerelease flag) is built entirely in Python to avoid shell/Python type mismatches. Tags containing `rc`, `beta`, or `alpha` are automatically marked as pre-releases.
Requires `RELEASE_TOKEN` secret with repository write permissions.
---
## Runner configuration
### Public runner setup
Workflows target public Gitea-compatible runners. The Linux build job runs inside a Docker container (`ubuntu:24.04`) for isolation and reproducibility. macOS and Windows jobs run directly on hosted runners.
**Required runner labels:**
- `ubuntu-latest` -- Linux builds (dockerized)
- `macos-13` -- macOS Intel builds
- `macos-14` -- macOS Apple Silicon builds
- `windows-latest` -- Windows builds
### Registering a self-hosted runner
If using self-hosted runners instead of hosted:
```bash
# Download the Gitea runner binary
curl -fsSL -o act_runner https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64
chmod +x act_runner
# Register with your Gitea instance
./act_runner register \
--instance https://git.kindred-systems.com \
--token <RUNNER_REGISTRATION_TOKEN> \
--labels ubuntu-latest:docker://ubuntu:24.04
# Start the runner
./act_runner daemon
```
For dockerized mode, the runner executes jobs inside containers. Ensure Docker is installed on the runner host.
The runner config file (`config.yaml`) should set:
```yaml
runner:
labels:
- "ubuntu-latest:docker://ubuntu:24.04"
container:
privileged: false
network: bridge
```
### Runner cleanup daemon
A cleanup script at `.gitea/runner/cleanup.sh` prevents disk exhaustion on self-hosted runners. It uses a tiered approach based on disk usage thresholds:
| Threshold | Action |
|-----------|--------|
| 70% | Docker cleanup (stopped containers, dangling images, build cache) |
| 80% | Purge act_runner cache entries older than 7 days, clean inactive workspaces |
| 90% | System cleanup (apt cache, old logs, journal vacuum to 100 MB) |
| 95% | Emergency: remove all act_runner cache entries and Docker images |
Install via the provided systemd units (`.gitea/runner/cleanup.service` and `.gitea/runner/cleanup.timer`) to run every 30 minutes:
```bash
sudo cp .gitea/runner/cleanup.sh /usr/local/bin/runner-cleanup.sh
sudo cp .gitea/runner/cleanup.service /etc/systemd/system/
sudo cp .gitea/runner/cleanup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
```
---
## Secrets
| Secret | Used by | Purpose |
|--------|---------|---------|
| `RELEASE_TOKEN` | `release.yml` (publish) | Gitea API token for creating releases |
| `SIGNING_KEY_ID` | macOS build (optional) | Apple Developer signing identity |
---
## Build tools
### pixi
[pixi](https://pixi.sh) manages all build dependencies via conda-forge. It ensures consistent toolchains (clang, cmake, Qt6, OpenCASCADE, etc.) across all platforms without relying on system packages.
Key pixi tasks (defined in root `pixi.toml`):
| Task | Description |
|------|-------------|
| `pixi run configure` | CMake configure (defaults to debug) |
| `pixi run build` | CMake build (defaults to debug) |
| `pixi run install` | CMake install |
| `pixi run test` | Run ctest |
| `pixi run freecad` | Launch built FreeCAD |
| `pixi run build-release` | CMake build (release) |
### CMake presets
Defined in `CMakePresets.json`. Release builds use:
| Platform | Preset | Compiler | Linker |
|----------|--------|----------|--------|
| Linux | `conda-linux-release` | clang/clang++ | mold |
| macOS | `conda-macos-release` | clang/clang++ | default |
| Windows | `conda-windows-release` | MSVC | default |
### ccache
Compiler cache is used across all builds to speed up incremental compilation. Cache is persisted between CI runs via `actions/cache` with date-based key rotation. Configuration:
- Max size: 4 GB
- Compression: zlib level 6
- Sloppy mode: include timestamps, PCH defines, time macros
- `CCACHE_BASEDIR`: set to workspace root (build workflow) or `$SRC_DIR` (rattler-build) for path normalization across runs
---
## Adding a new platform or package format
1. Create a bundle script at `package/rattler-build/<platform>/create_bundle.sh`
2. Add the platform to `package/rattler-build/pixi.toml` if needed
3. Add a new job to `release.yml` following the existing pattern
4. Add the new artifact pattern to the `publish-release` job's `find` command
5. Update the release body template with the new download entry
---
## Troubleshooting
### Build fails with missing system libraries
The Docker container installs only minimal dependencies. If a new dependency is needed, add it to the `apt-get install` step in the workflow. Check the pixi environment first -- most dependencies should come from conda-forge, not system packages.
### ccache misses are high
ccache misses spike when:
- The compiler version changes (pixi update)
- CMake presets change configuration flags
- First build of the day (date-based key rotates daily)
- New branch without a prior cache (falls back to `main` cache)
For release builds, ensure `build.sh` is correctly setting `CCACHE_DIR=/tmp/ccache-kindred-create` -- rattler-build does not forward environment variables from the workflow, so ccache config must be set inside the script.
Check `pixi run ccache -s` output (printed in the "Show ccache statistics" step) for hit/miss ratios. The "Prepare ccache" step also prints the full ccache configuration via `ccache -p`.
### Submodule checkout fails
Submodules pointing to internal Gitea instances require either:
- Public mirrors of the submodule repositories
- Runner network access to the Gitea instance
- Submodule URLs updated to public-facing addresses in `.gitmodules`
### macOS signing
Code signing and notarization require:
- `SIGNING_KEY_ID` secret set to the Apple Developer identity
- `SIGN_RELEASE=true` environment variable
- Valid Apple Developer account credentials in the runner keychain
Without signing, the DMG is created unsigned. Users will see Gatekeeper warnings.
### Windows NSIS installer not created
The NSIS installer requires:
- NSIS 3.x installed in the runner environment (available via conda or Chocolatey)
- `MAKE_INSTALLER=true` environment variable
- Chocolatey `shimgen.exe` for wrapper executables
If NSIS is unavailable, only the `.7z` portable archive is produced.

119
docs/COMPONENTS.md Normal file
View File

@@ -0,0 +1,119 @@
# Components
## ztools (command provider)
ZTools is no longer a standalone workbench. It registers as a command provider that
injects tools into the appropriate editing contexts via `WorkbenchManipulator` and
the `EditingContextResolver` system.
**Registered commands (9):**
| Command | Function | Injected Into |
|---------|----------|---------------|
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) | PartDesign Helper Features |
| `ZTools_DatumManager` | Manage existing datum objects | PartDesign Helper Features |
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) | PartDesign Modeling Features |
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation | PartDesign Transformation Features |
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Assembly |
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Assembly |
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles | Spreadsheet |
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment | Spreadsheet |
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation | Spreadsheet |
**Integration** via `_ZToolsManipulator` (WorkbenchManipulator) and `injectEditingCommands()`:
- Commands are injected into native workbench toolbars (PartDesign, Assembly, Spreadsheet)
- Context toolbar injections ensure commands appear when the relevant editing context is active
- PartDesign menu items inserted after `PartDesign_Boolean`
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.
---
## Origin commands (C++)
The Origin abstraction (`src/Gui/FileOrigin.h`) provides a backend-agnostic interface for document storage. Commands delegate to the active `FileOrigin` implementation (currently `LocalFileOrigin` for local files, `SiloOrigin` via `mods/silo/freecad/silo_origin.py` for Silo-tracked documents).
**Registered commands (5):**
| Command | Function | Icon |
|---------|----------|------|
| `Origin_Commit` | Commit changes as a new revision | `silo-commit` |
| `Origin_Pull` | Pull a specific revision from the origin | `silo-pull` |
| `Origin_Push` | Push local changes to the origin | `silo-push` |
| `Origin_Info` | Show document information from origin | `silo-info` |
| `Origin_BOM` | Show Bill of Materials for this document | `silo-bom` |
These appear in the File menu and "Origin Tools" toolbar across all workbenches (see `src/Gui/Workbench.cpp`).
---
## Silo workbench
**Registered commands (14):**
| Command | Function |
|---------|----------|
| `Silo_New` | Schema-driven item creation form — fetches categories and properties from the Silo API at runtime |
| `Silo_Open` | Open file from Silo database |
| `Silo_Save` | Save to Silo (create revision) |
| `Silo_Commit` | Commit current revision |
| `Silo_Pull` | Pull latest revision from server |
| `Silo_Push` | Push local changes to server |
| `Silo_Info` | View item metadata and history |
| `Silo_BOM` | Bill of materials dialog (BOM + Where Used) |
| `Silo_TagProjects` | Assign project tags |
| `Silo_Rollback` | Rollback to previous revision |
| `Silo_SetStatus` | Set revision status (draft/review/released/obsolete) |
| `Silo_Settings` | Configure API URL, projects dir, SSL certificates |
| `Silo_Auth` | Login/logout authentication panel |
**Global integration** via the origin system in `src/Gui/`:
- File toolbar: `Std_Origin` selector widget + `Std_New`/`Std_Open`/`Std_Save` (delegate to current origin)
- Origin Tools toolbar: `Origin_Commit`/`Origin_Pull`/`Origin_Push`/`Origin_Info`/`Origin_BOM` (auto-disable by capability)
- Silo origin registered at startup by `src/Mod/Create/InitGui.py`
**Dock panels:**
- Database Auth (2000ms) -- Login/logout and API token management
- Database Activity (4000ms) -- Real-time server event feed via SSE (Server-Sent Events) with automatic reconnection and exponential backoff
- Start Panel -- In-viewport landing page with recent files and Silo integration
**Server architecture:** Go REST API (38+ routes) + PostgreSQL + MinIO S3. Authentication via local (bcrypt), LDAP, or OIDC backends. SSE endpoint for real-time event streaming. See `docs/src/silo-server/` for server documentation.
**LibreOffice Calc extension** ([silo-calc](https://git.kindred-systems.com/kindred/silo-calc.git)): BOM management, item creation, and AI-assisted descriptions via OpenRouter API. Shares the same Silo REST API and auth token system via the shared [silo-client](https://git.kindred-systems.com/kindred/silo-client.git) package.
---
## Theme
**Canonical source:** `src/Gui/Stylesheets/KindredCreate.qss`
The PreferencePacks copy (`src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`) is generated at build time via `configure_file()` in `src/Gui/PreferencePacks/CMakeLists.txt`. Only the Stylesheets copy needs to be maintained.
Notable theme customizations beyond standard Catppuccin colors:
- `QGroupBox::indicator` styling to match `QCheckBox::indicator` (consistent checkbox appearance)
- `QLabel[haslink="true"]` link color (`#b4befe` Catppuccin Lavender) -- picked up by FreeCAD to set `QPalette::Link`
- Spanning-line tree branch indicators
---
## Icon infrastructure
### Qt resource icons (`src/Gui/Icons/`)
5 `silo-*` SVGs registered in `resource.qrc`, used by C++ Origin commands:
`silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-pull.svg`, `silo-push.svg`
### Silo module icons (`mods/silo/freecad/resources/icons/`)
10 SVGs loaded at runtime by the `_icon()` function in `silo_commands.py`:
`silo-auth.svg`, `silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-new.svg`, `silo-open.svg`, `silo-pull.svg`, `silo-push.svg`, `silo-save.svg`, `silo.svg`
### Missing icons
3 command icon names have no corresponding SVG file: `silo-tag`, `silo-rollback`, `silo-status`. The `_icon()` function returns an empty string for these, so `Silo_TagProjects`, `Silo_Rollback`, and `Silo_SetStatus` render without toolbar icons.
### Palette
All silo-* icons use the Catppuccin Mocha color scheme. See `kindred-icons/README.md` for palette specification and icon design standards.

159
docs/INTEGRATION_PLAN.md Normal file

File diff suppressed because one or more lines are too long

84
docs/KNOWN_ISSUES.md Normal file
View File

@@ -0,0 +1,84 @@
# Known Issues
## Issues
### Critical
1. ~~**QSS duplication.**~~ Resolved. The canonical QSS lives in `src/Gui/Stylesheets/KindredCreate.qss`. The PreferencePacks copy is now generated at build time via `configure_file()` in `src/Gui/PreferencePacks/CMakeLists.txt`. The unused `resources/preferences/KindredCreate/` directory has been removed.
2. ~~**WorkbenchManipulator timing.**~~ Resolved. ZTools is no longer a workbench. Commands are registered via a deferred `QTimer.singleShot(2000ms)` in `InitGui.py`, which activates dependent workbenches first, then imports ZTools commands and installs the `_ZToolsManipulator`. The `EditingContextResolver` handles toolbar visibility based on editing context.
3. ~~**Silo shortcut persistence.**~~ Resolved. `Silo_ToggleMode` removed; file operations now delegate to the selected origin via the unified origin system.
### High
4. **Silo authentication not production-hardened.** Local auth (bcrypt) works end-to-end. LDAP (FreeIPA) and OIDC (Keycloak) backends are coded but depend on infrastructure not yet deployed. FreeCAD client has `Silo_Auth` dock panel for login and API token management. Server has session middleware (`alexedwards/scs`), CSRF protection (`nosurf`), and role-based access control (admin/editor/viewer). Migration `009_auth.sql` adds users, api_tokens, and sessions tables.
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix in `src/Mod/Assembly/UtilsAssembly.py` extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane` and from shape vertex for `PartDesign::Point`. Does not handle empty shapes or non-planar datum objects.
### Medium
7. **`Silo_BOM` requires Silo-tracked document.** Depends on `SiloPartNumber` property. Unregistered documents show a warning with no registration path.
8. **PartDesign menu insertion fragility.** `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`. If upstream renames this command, insertions silently fail.
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
11. **Missing Silo icons.** Three commands reference icons that don't exist: `silo-tag.svg` (`Silo_TagProjects`), `silo-rollback.svg` (`Silo_Rollback`), `silo-status.svg` (`Silo_SetStatus`). The `_icon()` function returns an empty string, so these commands render without toolbar icons.
### Fixed (retain for reference)
12. **OndselSolver Newton-Raphson convergence.** `NewtonRaphson::isConvergedToNumericalLimit()` compared `dxNorms->at(iterNo)` to itself instead of `dxNorms->at(iterNo - 1)`. This prevented convergence detection on complex assemblies, causing solver exhaustion and "grounded object moved" warnings. Fixed in Kindred fork (`src/3rdParty/OndselSolver`). Needs upstreaming to `FreeCAD/OndselSolver`.
13. **Assembly solver crash on document restore.** `AssemblyObject::onChanged()` called `updateSolveStatus()` when the Group property changed during document restore, triggering the solver while child objects were still deserializing (SIGSEGV). Fixed with `isRestoring()` and `isPerformingTransaction()` guards at `src/Mod/Assembly/App/AssemblyObject.cpp:143`.
14. **`DlgSettingsGeneral::applyMenuIconSize` visibility.** The method was `private` but called from `StartupProcess.cpp`. Fixed by moving to `public` (PR #49). Also required `Dialog::` namespace qualifier in `StartupProcess.cpp`.
---
## Incomplete features
### Silo
| Feature | Status | Notes |
|---------|--------|-------|
| Authentication | Local auth complete | LDAP/OIDC backends coded, pending infrastructure. Auth dock panel available. |
| CSRF protection | Implemented | `nosurf` library on web form routes |
| File locking | Not implemented | Needed to prevent concurrent edits |
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
| Part number date segments | Broken | `formatDate()` returns error |
| Location/inventory APIs | Tables exist, no handlers | |
| CSV import rollback | Not implemented | `bom_handlers.go` |
| SSE event streaming | Implemented | Reconnect logic with exponential backoff |
| Database Activity panel | Implemented | Dock panel showing real-time server events |
| Start panel | Implemented | In-viewport start page with recent files and Silo integration |
### ztools
| Feature | Status | Notes |
|---------|--------|-------|
| Tangent-to-cylinder attachment | Manual fallback | No vertex ref in UI |
| Angled datum live editing | Incomplete | AttachmentOffset not updated in panel |
| Assembly pattern undo | Not implemented | |
---
## Next steps
1. **Authentication hardening** -- Deploy FreeIPA and Keycloak infrastructure. End-to-end test LDAP and OIDC flows. Harden token rotation and session expiry.
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
6. **QSS consolidation** -- Eliminate the 3-copy QSS duplication via build-time copy or symlinks. The canonical source is `resources/preferences/KindredCreate/KindredCreate.qss`.
7. **Update notification UI** -- Display in-app notification when a new release is available (issue #30). The update checker backend is already implemented.

31
docs/OVERVIEW.md Normal file
View File

@@ -0,0 +1,31 @@
# Kindred Create
**Last updated:** 2026-02-08
**Branch:** main @ `cf523f1d87a`
**Kindred Create:** v0.1.0
**FreeCAD base:** v1.0.0
## Documentation
| Document | Contents |
|----------|----------|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Bootstrap flow, source layout, submodules |
| [COMPONENTS.md](COMPONENTS.md) | ztools, Silo, Origin commands, theme, icons |
| [KNOWN_ISSUES.md](KNOWN_ISSUES.md) | Bugs, incomplete features, next steps |
| [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) | Architecture layers, integration phases |
| [CI_CD.md](CI_CD.md) | Build and release workflows |
## Submodules
| Submodule | Path | Source | Pinned commit |
|-----------|------|--------|---------------|
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | `3298d1c` |
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | `f9924d3` |
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | `fe41fa3` |
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | `756c91a` |
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | `01e242e` |
| googletest | `tests/lib` | `github.com/google/googletest` | `56efe39` |
The silo submodule was split from a monorepo into three repos: `silo-client` (shared Python API client), `silo-mod` (FreeCAD workbench, used as Create's submodule), and `silo-calc` (LibreOffice Calc extension). The `silo-mod` repo includes `silo-client` as its own submodule.
OndselSolver is forked from `github.com/FreeCAD/OndselSolver` to carry a Newton-Raphson convergence fix (see [KNOWN_ISSUES.md](KNOWN_ISSUES.md#12)).

16
docs/book.toml Normal file
View File

@@ -0,0 +1,16 @@
[book]
title = "Kindred Create Documentation"
authors = ["Kindred Systems LLC"]
language = "en"
src = "src"
[build]
build-dir = "book"
[output.html]
default-theme = "coal"
preferred-dark-theme = "coal"
git-repository-url = "https://git.kindred-systems.com/kindred/create"
git-repository-icon = "fas-code-fork"
additional-css = ["theme/kindred.css"]
no-section-label = false

63
docs/src/SUMMARY.md Normal file
View File

@@ -0,0 +1,63 @@
# Summary
[Introduction](./introduction.md)
---
# User Guide
- [Getting Started](./guide/getting-started.md)
- [Installation](./guide/installation.md)
- [Building from Source](./guide/building.md)
- [Workbenches](./guide/workbenches.md)
- [ztools](./guide/ztools.md)
- [Silo](./guide/silo.md)
# Architecture
- [Overview](./architecture/overview.md)
- [Python as Source of Truth](./architecture/python-source-of-truth.md)
- [Silo Server](./architecture/silo-server.md)
- [Signal Architecture](./architecture/signal-architecture.md)
- [OndselSolver](./architecture/ondsel-solver.md)
# Development
- [Contributing](./development/contributing.md)
- [Code Quality](./development/code-quality.md)
- [Repository Structure](./development/repo-structure.md)
- [Build System](./development/build-system.md)
- [Gui Module Build](./development/gui-build-integration.md)
# Silo Server
- [Overview](./silo-server/overview.md)
- [Specification](./silo-server/SPECIFICATION.md)
- [Configuration](./silo-server/CONFIGURATION.md)
- [Deployment](./silo-server/DEPLOYMENT.md)
- [Authentication](./silo-server/AUTH.md)
- [Middleware](./silo-server/AUTH_MIDDLEWARE.md)
- [User Guide](./silo-server/AUTH_USER_GUIDE.md)
- [BOM Merge](./silo-server/BOM_MERGE.md)
- [Calculated Fields](./silo-server/CALC_EXTENSION.md)
- [Component Audit](./silo-server/COMPONENT_AUDIT.md)
- [Status System](./silo-server/STATUS.md)
- [Gap Analysis](./silo-server/GAP_ANALYSIS.md)
- [Frontend Spec](./silo-server/frontend-spec.md)
- [Installation](./silo-server/INSTALL.md)
- [Roadmap](./silo-server/ROADMAP.md)
# Reference
- [Configuration](./reference/configuration.md)
- [Glossary](./reference/glossary.md)
# C++ API Reference
- [FileOrigin Interface](./reference/cpp-file-origin.md)
- [LocalFileOrigin](./reference/cpp-local-file-origin.md)
- [OriginManager](./reference/cpp-origin-manager.md)
- [CommandOrigin](./reference/cpp-command-origin.md)
- [OriginSelectorWidget](./reference/cpp-origin-selector-widget.md)
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)
- [Creating a Custom Origin (C++)](./reference/cpp-custom-origin-guide.md)

View File

@@ -0,0 +1,27 @@
# OndselSolver
OndselSolver is the assembly constraint solver used by FreeCAD's Assembly workbench. Kindred Create vendors a fork of the solver as a git submodule.
- **Path:** `src/3rdParty/OndselSolver/`
- **Source:** `git.kindred-systems.com/kindred/solver` (Kindred fork)
## How it works
The solver uses a **Lagrangian constraint formulation** to resolve assembly constraints (mates, joints, fixed positions). Given a set of parts with geometric constraints between them, it computes positions and orientations that satisfy all constraints simultaneously.
The Assembly workbench (`src/Mod/Assembly/`) calls the solver whenever constraints are added or modified. Kindred Create has patches to `Assembly/` that extend `findPlacement()` for better datum and origin handling.
## Why a fork
The solver is forked from the upstream Ondsel project for:
- **Pinned stability** — the submodule is pinned to a known-good commit
- **Potential modifications** — the fork allows Kindred-specific patches if needed
- **Availability** — hosted on Kindred's Gitea instance for reliable access
## Future: GNN solver
There are plans to explore a Graph Neural Network (GNN) approach to constraint solving that could complement or supplement the Lagrangian solver for specific use cases. This is not yet implemented.
## Related: GSL
The `src/3rdParty/GSL/` submodule is Microsoft's Guidelines Support Library (`github.com/microsoft/GSL`), providing C++ core guidelines utilities like `gsl::span` and `gsl::not_null`. It is a build dependency, not related to the constraint solver.

View File

@@ -0,0 +1,78 @@
# Architecture Overview
Kindred Create is structured as a thin integration layer on top of FreeCAD. The design follows three principles:
1. **Minimal core modifications** — prefer submodule addons over patching FreeCAD internals
2. **Graceful degradation** — Create runs without ztools or Silo if submodules are missing
3. **Pure Python addons** — workbenches follow FreeCAD's standard addon pattern
## Three-layer model
```
┌─────────────────────────────────┐
│ FreeCAD Documents (.FCStd) │ Python source of truth
│ Workbench logic (Python) │
├─────────────────────────────────┤
│ PostgreSQL │ Silo metadata, revisions, BOM
├─────────────────────────────────┤
│ MinIO (S3-compatible) │ Binary file storage cache
└─────────────────────────────────┘
```
FreeCAD documents are the authoritative representation of CAD data. Silo's PostgreSQL database stores metadata (part numbers, revisions, BOM relationships) and MinIO stores the binary `.FCStd` files. The FreeCAD workbench synchronizes between local files and the server.
## Source layout
```
create/
├── src/App/ # Core application (C++)
├── src/Base/ # Foundation classes (C++)
├── src/Gui/ # GUI framework (C++ + Qt6 + QSS)
│ ├── Stylesheets/ # KindredCreate.qss theme
│ ├── PreferencePacks/ # Theme preference pack
│ ├── Icons/ # silo-*.svg origin icons
│ ├── FileOrigin.* # Abstract file origin interface
│ └── OriginManager.* # Origin lifecycle management
├── src/Mod/ # ~37 FreeCAD modules
│ ├── Create/ # Kindred bootstrap module
│ ├── Assembly/ # Assembly workbench (Kindred patches)
│ ├── PartDesign/ # Part Design (stock + ztools injection)
│ └── ... # Other stock FreeCAD modules
├── mods/
│ ├── ztools/ # Datum/pattern/pocket workbench (submodule)
│ └── silo/ # Parts database workbench (submodule)
└── src/3rdParty/
├── OndselSolver/ # Assembly solver (submodule)
└── GSL/ # Guidelines Support Library (submodule)
```
## Bootstrap sequence
1. FreeCAD core initializes, discovers `src/Mod/Create/`
2. `Init.py` runs `setup_kindred_addons()` — adds `mods/ztools/ztools` and `mods/silo/freecad` to `sys.path`, executes their `Init.py`
3. GUI phase: `InitGui.py` runs `setup_kindred_workbenches()` — executes addon `InitGui.py` files to register workbenches
4. Deferred QTimer cascade:
- **1500ms** — Register Silo as a file origin
- **2000ms** — Dock the Silo auth panel
- **3000ms** — Check for Silo first-start configuration
- **4000ms** — Dock the Silo activity panel
- **10000ms** — Check for application updates
The QTimer cascade exists because FreeCAD's startup is not fully synchronous — Silo registration must wait for the GUI framework to be ready.
## Origin system
The origin system is Kindred's primary addition to FreeCAD's GUI layer:
- **`FileOrigin`** — abstract C++ interface for file backends
- **`LocalFileOrigin`** — default implementation (local filesystem)
- **`SiloOrigin`** — Silo database backend (registered by the Python addon)
- **`OriginManager`** — manages origin lifecycle, switching, capability queries
- **`OriginSelectorWidget`** — dropdown in the File toolbar
- **`CommandOrigin.cpp`** — Commit / Pull / Push / Info / BOM commands that delegate to the active origin
## Module interaction
- **ztools** injects commands into PartDesign via `_ZToolsPartDesignManipulator`
- **Silo** registers as a `FileOrigin` backend via `silo_origin.register_silo_origin()`
- **Create module** is glue only — no feature code, just bootstrap and version management

View File

@@ -0,0 +1,26 @@
# Python as Source of Truth
In Kindred Create's architecture, FreeCAD documents (`.FCStd` files) are the authoritative representation of all CAD data. The Silo database and MinIO storage are caches and metadata indexes — they do not define the model.
## How it works
An `.FCStd` file is a ZIP archive containing:
- XML documents describing the parametric model tree
- BREP geometry files for each shape
- Thumbnail images
- Embedded spreadsheets and metadata
When a user runs **Commit**, the workbench uploads the entire `.FCStd` file to MinIO and records metadata (part number, revision, timestamp, commit message) in PostgreSQL. When a user runs **Pull**, the workbench downloads the `.FCStd` from MinIO and opens it locally.
## Why this design
- **No data loss** — the complete model is always in the `.FCStd` file, never split across systems
- **Offline capability** — engineers can work without a server connection
- **FreeCAD compatibility** — files are standard FreeCAD documents, openable in stock FreeCAD
- **Simple sync model** — the unit of transfer is always a whole file, avoiding merge conflicts in binary geometry
## Trade-offs
- **Large files** — `.FCStd` files can grow large with complex assemblies; every commit stores the full file
- **No partial sync** — you cannot pull a single feature or component; it is all or nothing
- **Conflict resolution** — two users editing the same file must resolve conflicts manually (last-commit-wins by default)

View File

@@ -0,0 +1,274 @@
# Signal Architecture
Kindred Create uses two signal systems side by side: **fastsignals** for domain and application events, and **Qt signals** for UI framework integration. This page explains why both exist, where each is used, and the patterns for working with them.
## Why two signal systems
Qt signals require the `Q_OBJECT` macro, MOC preprocessing, and a `QObject` base class. This makes them the right tool for widget-to-widget communication but a poor fit for domain classes like `FileOrigin`, `OriginManager`, and `App::Document` that are not `QObject` subclasses and should not depend on the Qt meta-object system.
fastsignals is a header-only C++ library that provides type-safe signals with lambda support, RAII connection management, and no build-tool preprocessing. It lives at `src/3rdParty/FastSignals/` and is linked into the Gui module via CMake.
| | Qt signals | fastsignals |
|---|-----------|-------------|
| **Requires** | `Q_OBJECT`, MOC, `QObject` base | Nothing (header-only) |
| **Dispatch** | Event loop (can be queued) | Synchronous, immediate |
| **Connection** | `QObject::connect()` | `signal.connect(lambda)` |
| **Lifetime** | Tied to `QObject` parent-child tree | `scoped_connection` RAII |
| **Thread model** | Can queue across threads | Fires on emitter's thread |
| **Slot types** | Slots, lambdas, `std::function` | Lambdas, `std::function`, function pointers |
## Where each is used
### fastsignals — domain events
These are events about application state that multiple independent listeners may care about. The emitting class is not a `QObject`.
**FileOrigin** (`src/Gui/FileOrigin.h`):
```cpp
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
```
**OriginManager** (`src/Gui/OriginManager.h`):
```cpp
fastsignals::signal<void(const std::string&)> signalOriginRegistered;
fastsignals::signal<void(const std::string&)> signalOriginUnregistered;
fastsignals::signal<void(const std::string&)> signalCurrentOriginChanged;
fastsignals::signal<void(App::Document*, const std::string&)> signalDocumentOriginChanged;
```
**Gui::Document** (`src/Gui/Document.h`):
```cpp
mutable fastsignals::signal<void(const ViewProviderDocumentObject&)> signalNewObject;
mutable fastsignals::signal<void(const ViewProviderDocumentObject&)> signalDeletedObject;
mutable fastsignals::signal<void(const ViewProviderDocumentObject&,
const App::Property&)> signalChangedObject;
// ~9 more document-level signals
```
**Gui::Application** (`src/Gui/Application.h`):
```cpp
fastsignals::signal<void(const Gui::Document&, bool)> signalNewDocument;
fastsignals::signal<void(const Gui::Document&)> signalDeleteDocument;
fastsignals::signal<void(const Gui::ViewProvider&)> signalNewObject;
// ~7 more application-level signals
```
### Qt signals — UI interaction
These are events between Qt widgets, actions, and framework components where `QObject` is already the base class and the Qt event loop handles dispatch.
**OriginSelectorWidget** (a `QToolButton` subclass):
```cpp
// QActionGroup::triggered → onOriginActionTriggered
connect(m_originActions, &QActionGroup::triggered,
this, &OriginSelectorWidget::onOriginActionTriggered);
// QAction::triggered → onManageOriginsClicked
connect(m_manageAction, &QAction::triggered,
this, &OriginSelectorWidget::onManageOriginsClicked);
```
### The boundary
The pattern is consistent: **fastsignals for observable state changes in domain classes, Qt signals for UI framework plumbing**. A typical flow crosses the boundary once:
```
OriginManager emits signalCurrentOriginChanged (fastsignals)
→ OriginSelectorWidget::onCurrentOriginChanged (lambda listener)
→ updates QToolButton text/icon (Qt API calls)
```
## fastsignals API
### Declaring a signal
Signals are public member variables with a template signature:
```cpp
fastsignals::signal<void(const std::string&)> signalSomethingHappened;
```
Use `mutable` if the signal needs to fire from `const` methods (as `Gui::Document` does):
```cpp
mutable fastsignals::signal<void(int)> signalReadOnlyEvent;
```
Name signals with the `signal` prefix by convention.
### Emitting
Call the signal like a function:
```cpp
signalSomethingHappened("hello");
```
All connected slots execute **synchronously**, in connection order, before the call returns. There is no event loop queuing.
### Connecting
`signal.connect()` accepts any callable and returns a connection object:
```cpp
auto conn = mySignal.connect([](const std::string& s) {
Base::Console().log("Got: %s\n", s.c_str());
});
```
### Connection types
| Type | RAII | Copyable | Use case |
|------|------|----------|----------|
| `fastsignals::connection` | No | Yes | Long-lived, manually managed |
| `fastsignals::scoped_connection` | Yes | No (move only) | Member variable, automatic cleanup |
| `fastsignals::advanced_connection` | No | No | Temporary blocking via `shared_connection_block` |
`scoped_connection` is the standard choice for class members. It disconnects automatically when destroyed.
### Disconnecting
```cpp
conn.disconnect(); // Explicit
// or let scoped_connection destructor handle it
```
## Connection patterns
### Pattern 1: Scoped member connections
The most common pattern. Store `scoped_connection` as a class member and connect in the constructor or an `attach()` method.
```cpp
class MyListener {
fastsignals::scoped_connection m_conn;
public:
MyListener(OriginManager* mgr)
{
m_conn = mgr->signalOriginRegistered.connect(
[this](const std::string& id) { onRegistered(id); }
);
}
// Destructor auto-disconnects via m_conn
~MyListener() = default;
private:
void onRegistered(const std::string& id) { /* ... */ }
};
```
### Pattern 2: Explicit disconnect in destructor
`OriginSelectorWidget` disconnects explicitly before destruction to prevent any signal delivery during teardown:
```cpp
OriginSelectorWidget::~OriginSelectorWidget()
{
disconnectSignals(); // m_conn*.disconnect()
}
```
This is defensive — `scoped_connection` would disconnect on its own, but explicit disconnection avoids edge cases where a signal fires between member destruction order.
### Pattern 3: DocumentObserver base class
`Gui::DocumentObserver` wraps ~10 document signals behind virtual methods:
```cpp
class DocumentObserver {
using Connection = fastsignals::scoped_connection;
Connection connectDocumentCreatedObject;
Connection connectDocumentDeletedObject;
// ...
void attachDocument(Document* doc); // connects all
void detachDocument(); // disconnects all
};
```
Subclasses override `slotCreatedObject()`, `slotDeletedObject()`, etc. This pattern avoids repeating connection boilerplate in every document listener.
### Pattern 4: Temporary blocking
`advanced_connection` supports blocking a slot without disconnecting:
```cpp
auto conn = signal.connect(handler, fastsignals::advanced_tag());
fastsignals::shared_connection_block block(conn, true); // blocked
signal(); // handler does NOT execute
block.unblock();
signal(); // handler executes
```
Use this to prevent recursive signal handling.
## Thread safety
### What is thread-safe
- **Emitting** a signal from any thread (internal spin mutex protects the slot list).
- **Connecting and disconnecting** from any thread, even while slots are executing on another thread.
### What is not thread-safe
- **Accessing the same `connection` object** from multiple threads. Protect with your own mutex or keep connection objects thread-local.
- **Slot execution context.** Slots run on the emitter's thread. If a fastsignal fires on a background thread and the slot touches Qt widgets, you must marshal to the main thread:
```cpp
mgr->signalOriginRegistered.connect([this](const std::string& id) {
QMetaObject::invokeMethod(this, [this, id]() {
// Now on the main thread — safe to update UI
updateUI(id);
}, Qt::QueuedConnection);
});
```
In practice, all origin and document signals in Kindred Create fire on the main thread, so this marshalling is not currently needed. It would become necessary if background workers emitted signals.
## Performance
- **Emission:** O(n) where n = connected slots. No allocation, no event loop overhead.
- **Connection:** O(1) with spin mutex.
- **Memory:** Each signal stores a shared pointer to a slot vector. Each `scoped_connection` is ~16 bytes.
- fastsignals is rarely a bottleneck. Profile before optimising signal infrastructure.
## Adding a new signal
1. Declare the signal as a public member (or `mutable` if emitting from const methods).
2. Name it with the `signal` prefix.
3. Emit it at the appropriate point in your code.
4. Listeners store `scoped_connection` members and connect via lambdas.
5. Document the signal's signature and when it fires.
Do not create a fastsignal for single-listener scenarios — a direct method call is simpler.
## Common mistakes
**Dangling `this` capture.** If a lambda captures `this` and the object is destroyed before the connection, the next emission crashes. Always store the connection as a `scoped_connection` member so it disconnects on destruction.
**Assuming queued dispatch.** fastsignals are synchronous. A slot that blocks will block the emitter. Keep slots fast or offload work to a background thread.
**Forgetting `mutable`.** If you need to emit from a `const` method, the signal member must be `mutable`. Otherwise the compiler rejects the call.
**Copying `scoped_connection`.** It is move-only. Use `std::move()` when putting connections into containers:
```cpp
std::vector<fastsignals::scoped_connection> conns;
conns.push_back(std::move(conn)); // OK
```
## See also
- [OriginManager](../reference/cpp-origin-manager.md) — signal catalog for origin lifecycle events
- [FileOrigin Interface](../reference/cpp-file-origin.md) — `signalConnectionStateChanged`
- [OriginSelectorWidget](../reference/cpp-origin-selector-widget.md) — listener patterns in practice
- `src/3rdParty/FastSignals/` — library source and headers

View File

@@ -0,0 +1,11 @@
# Silo Server
The Silo server architecture is documented in the dedicated [Silo Server](../silo-server/overview.md) section.
- [Overview](../silo-server/overview.md) — Architecture, stack, and key features
- [Specification](../silo-server/SPECIFICATION.md) — Full API specification with 38+ routes
- [Configuration](../silo-server/CONFIGURATION.md) — YAML config reference
- [Deployment](../silo-server/DEPLOYMENT.md) — Docker Compose, systemd, production setup
- [Authentication](../silo-server/AUTH.md) — LDAP, OIDC, and local auth backends
- [Status System](../silo-server/STATUS.md) — Revision lifecycle states
- [Gap Analysis](../silo-server/GAP_ANALYSIS.md) — Current gaps and planned improvements

View File

@@ -0,0 +1,72 @@
# Build System
Kindred Create uses **CMake** for build configuration, **pixi** (conda-based) for dependency management and task running, and **ccache** for compilation caching.
## Overview
- **CMake** >= 3.22.0
- **Ninja** generator (via conda presets)
- **pixi** manages all dependencies — compilers, Qt6, OpenCASCADE, Python, etc.
- **ccache** with 4 GB max, zlib compression level 6, sloppy mode
- **mold** linker on Linux for faster link times
## CMake configuration
The root `CMakeLists.txt` defines:
- **Kindred Create version:** `0.1.0` (via `KINDRED_CREATE_VERSION`)
- **FreeCAD base version:** `1.0.0` (via `FREECAD_VERSION`)
- CMake policy settings for compatibility
- ccache auto-detection
- Submodule dependency checks
- Library setup: yaml-cpp, fmt, ICU
### Version injection
The version flows from CMake to Python via `configure_file()`:
```
CMakeLists.txt (KINDRED_CREATE_VERSION = "0.1.0")
→ src/Mod/Create/version.py.in (template)
→ build/*/Mod/Create/version.py (generated)
→ update_checker.py (imports VERSION)
```
## CMake presets
Defined in `CMakePresets.json`:
| Preset | Platform | Build type |
|--------|----------|------------|
| `conda-linux-debug` | Linux | Debug |
| `conda-linux-release` | Linux | Release |
| `conda-macos-debug` | macOS | Debug |
| `conda-macos-release` | macOS | Release |
| `conda-windows-debug` | Windows | Debug |
| `conda-windows-release` | Windows | Release |
All presets inherit from a hidden `common` base and a hidden `conda` base (Ninja generator, conda toolchain).
## cMake/ helper modules
The `cMake/` directory contains CMake helper macros inherited from FreeCAD:
- **FreeCAD_Helpers** — macros for building FreeCAD modules
- Platform detection modules
- Dependency finding modules (Find*.cmake)
## Dependencies
Core dependencies managed by pixi (from `pixi.toml`):
| Category | Packages |
|----------|----------|
| Build | cmake, ninja, swig, compilers (clang/gcc) |
| CAD kernel | occt 7.8, coin3d, opencamlib, pythonocc-core |
| UI | Qt6 6.8, PySide6, pyside6 |
| Math | eigen, numpy, scipy, sympy |
| Data | hdf5, vtk, smesh, ifcopenshell |
| Python | 3.11 (< 3.12), pip, freecad-stubs |
Platform-specific extras:
- **Linux:** clang, kernel-headers, mesa, X11, libspnav
- **macOS:** sed
- **Windows:** pthreads-win32

View File

@@ -0,0 +1,40 @@
# Code Quality
## Formatting and linting
### C/C++
- **Formatter:** clang-format (config in `.clang-format`)
- **Static analysis:** clang-tidy (config in `.clang-tidy`)
### Python
- **Formatter:** black with 100-character line length
- **Linter:** pylint (config in `.pylintrc`)
## Pre-commit hooks
The repository uses [pre-commit](https://pre-commit.com/) to run formatters and linters automatically on staged files:
```bash
pip install pre-commit
pre-commit install
```
Configured hooks (`.pre-commit-config.yaml`):
- `trailing-whitespace` — remove trailing whitespace
- `end-of-file-fixer` — ensure files end with a newline
- `check-yaml` — validate YAML syntax
- `check-added-large-files` — prevent accidental large file commits
- `mixed-line-ending` — normalize line endings
- `black` — Python formatting (100 char lines)
- `clang-format` — C/C++ formatting
## Scope
Pre-commit hooks are configured to run on specific directories:
- `src/Base/`, `src/Gui/`, `src/Main/`, `src/Tools/`
- `src/Mod/Assembly/`, `src/Mod/BIM/`, `src/Mod/CAM/`, `src/Mod/Draft/`, `src/Mod/Fem/`, and other stock modules
- `tests/src/`
Excluded: generated files, vendored libraries (`QSint/`, `Quarter/`, `3Dconnexion/navlib`), and binary formats.

View File

@@ -0,0 +1,52 @@
# Contributing
Kindred Create is maintained at [git.kindred-systems.com/kindred/create](https://git.kindred-systems.com/kindred/create). Contributions are submitted as pull requests against the `main` branch.
## Getting started
```bash
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
cd create
pixi run configure
pixi run build
pixi run freecad
```
See [Building from Source](../guide/building.md) for the full development setup.
## Branch and PR workflow
1. Create a feature branch from `main`:
```bash
git checkout -b feat/my-feature main
```
2. Make your changes, commit with conventional commit messages (see below).
3. Push and open a pull request against `main`.
4. CI builds and tests run automatically on all PRs.
## Commit messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
| Prefix | Purpose |
|--------|---------|
| `feat:` | New feature |
| `fix:` | Bug fix |
| `chore:` | Maintenance, dependencies |
| `docs:` | Documentation only |
| `art:` | Icons, theme, visual assets |
Scope is optional but encouraged:
- `feat(ztools): add datum point creation mode`
- `fix(gui): correct menu icon size on Wayland`
- `chore: update silo submodule`
## Reporting issues
Report issues at the [issue tracker](https://git.kindred-systems.com/kindred/create/issues). When reporting:
1. Note whether the issue involves Kindred Create additions (ztools, Silo, theme) or base FreeCAD
2. Include version info from **Help > About FreeCAD > Copy to clipboard**
3. Provide reproduction steps and attach example files (FCStd as ZIP) if applicable
For base FreeCAD issues, also check the [FreeCAD issue tracker](https://github.com/FreeCAD/FreeCAD/issues).

View File

@@ -0,0 +1,239 @@
# Gui Module Build Integration
The `FreeCADGui` shared library is the main GUI module. It contains the origin system, toolbar widgets, 3D viewport, command dispatch, and the Python bridge layer. This page explains how it is built, what it links against, and how to add new source files.
## Target overview
```cmake
add_library(FreeCADGui SHARED)
```
Built when `BUILD_GUI=ON` (the default). The output is a shared library installed to `${CMAKE_INSTALL_LIBDIR}`.
**CMake file:** `src/Gui/CMakeLists.txt`
## Dependency chain
```
FreeCADGui (SHARED)
├── FreeCADApp (core application module)
├── libfastsignals (STATIC, from src/3rdParty/FastSignals/)
├── Qt6::Core, Widgets, OpenGL, OpenGLWidgets, Network,
│ PrintSupport, Svg, SvgWidgets, UiTools, Xml
├── Coin3D / Quarter (3D scene graph)
├── PySide6 / Shiboken6 (Python-Qt bridge, if FREECAD_USE_PYSIDE=ON)
├── PyCXX (C++-Python interop, header-only)
├── Boost (various components)
├── OpenGL
├── yaml-cpp
├── ICU (Unicode)
└── (optional) 3Dconnexion, Tracy profiler, VR
```
### fastsignals
fastsignals is built as a **static library** with position-independent code:
```
src/3rdParty/FastSignals/
└── libfastsignals/
├── CMakeLists.txt # builds libfastsignals.a (STATIC, -fPIC)
├── include/fastsignals/ # public headers (INTERFACE include dir)
└── src/ # implementation
```
Linked into FreeCADGui via:
```cmake
target_link_libraries(FreeCADGui ... libfastsignals ...)
target_include_directories(FreeCADGui SYSTEM PRIVATE ${FastSignals_INCLUDE_DIRS})
```
The `fastsignals/signal.h` header is also included in `src/Gui/PreCompiled.h` so it is available without explicit `#include` in any Gui source file.
## Source file organisation
Source files are grouped into named blocks in `CMakeLists.txt`. Each group becomes a Visual Studio filter / IDE source group.
| Group variable | Contains | Origin system files in this group |
|----------------|----------|-----------------------------------|
| `Command_CPP_SRCS` | Command classes | `CommandOrigin.cpp` |
| `Widget_CPP_SRCS` | Toolbar/dock widgets | `OriginSelectorWidget.h/.cpp` |
| `Workbench_CPP_SRCS` | Workbench infrastructure | `OriginManager.h/.cpp`, `OriginManagerDialog.h/.cpp` |
| `FreeCADGui_CPP_SRCS` | Core Gui classes | `FileOrigin.h/.cpp`, `FileOriginPython.h/.cpp` |
All groups are collected into `FreeCADGui_SRCS` and added to the target:
```cmake
target_sources(FreeCADGui PRIVATE ${FreeCADGui_SRCS})
```
## Adding new source files
1. Create your `.h` and `.cpp` files in `src/Gui/`.
2. Add them to the appropriate source group in `src/Gui/CMakeLists.txt`. For origin system code, follow the existing pattern:
- Widget → `Widget_CPP_SRCS`
- Command → `Command_CPP_SRCS`
- Manager/infrastructure → `Workbench_CPP_SRCS`
- Core class → `FreeCADGui_CPP_SRCS`
```cmake
SET(FreeCADGui_CPP_SRCS
...
MyNewOrigin.cpp
MyNewOrigin.h
...
)
```
3. If the file has a `Q_OBJECT` macro, CMake's `AUTOMOC` handles MOC generation automatically. No manual steps needed.
4. If adding Python bindings via PyCXX, add the `.pyi` stub and register it:
```cmake
generate_from_py(MyNewOrigin)
```
5. If linking a new external library:
```cmake
list(APPEND FreeCADGui_LIBS MyNewLibrary)
```
6. Reconfigure and build:
```bash
pixi run configure
pixi run build
```
No changes are needed to `CommandOrigin.cpp`, `OriginSelectorWidget.cpp`, or `Workbench.cpp` when adding a new origin — those modules discover origins dynamically through `OriginManager` at runtime.
## Qt integration
### Modules linked
QtCore, QtWidgets, QtOpenGL, QtOpenGLWidgets, QtPrintSupport, QtSvg, QtSvgWidgets, QtNetwork, QtUiTools, QtXml. Qt version `>=6.8,<6.9` is specified in `pixi.toml`.
### MOC (Meta Object Compiler)
Handled automatically by CMake's `AUTOMOC` for any header containing `Q_OBJECT`. Exception: `GraphvizView` has manual MOC commands due to moc-from-cpp requirements.
### UI files
~100 `.ui` files are compiled by Qt's `uic` into C++ headers. Declared in the `Gui_UIC_SRCS` block.
### Resources
Icons are embedded via `Icons/resource.qrc`. Translations use Qt Linguist (`.ts` → `.qm`) with auto-generated resource files.
## Theme and stylesheet build
Stylesheets are **copied, not compiled**. The canonical stylesheet is `src/Gui/Stylesheets/KindredCreate.qss`.
`src/Gui/Stylesheets/CMakeLists.txt` defines a custom target that copies `.qss` files and SVG/PNG images to the build tree:
```cmake
fc_copy_sources(Stylesheets_data
"${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/Stylesheets"
${Stylesheets_Files} ${Images_Files} ...)
```
`src/Gui/PreferencePacks/CMakeLists.txt` copies the same stylesheet into the preference pack directory using `configure_file(... COPYONLY)`, avoiding a duplicate source:
```cmake
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/../Stylesheets/KindredCreate.qss
${CMAKE_BINARY_DIR}/.../PreferencePacks/KindredCreate/KindredCreate.qss
COPYONLY)
```
Edit only the canonical file in `Stylesheets/` — the preference pack copy is generated.
## Version constants
Defined in the top-level `CMakeLists.txt` and injected as compiler definitions:
```cmake
set(KINDRED_CREATE_VERSION "0.1.0")
set(FREECAD_VERSION "1.0.0")
add_definitions(-DKINDRED_CREATE_VERSION="${KINDRED_CREATE_VERSION}")
add_definitions(-DFREECAD_VERSION="${FREECAD_VERSION}")
```
Available in C++ code as preprocessor macros. The Python update checker uses `version.py` generated from `version.py.in` at build time with `configure_file()`.
## Precompiled headers
Controlled by `FREECAD_USE_PCH` (off by default in conda builds):
```cmake
if(FREECAD_USE_PCH)
target_precompile_headers(FreeCADGui PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:"PreCompiled.h">)
endif()
```
`PreCompiled.h` includes `<fastsignals/signal.h>`, all Qt headers (via `QtAll.h`), Coin3D headers, Boost, and Xerces. QSint and Quarter sources are excluded from PCH via `SKIP_PRECOMPILE_HEADERS`.
## Build presets
Defined in `CMakePresets.json`:
| Preset | Platform | Build type |
|--------|----------|------------|
| `conda-linux-debug` | Linux | Debug |
| `conda-linux-release` | Linux | Release |
| `conda-macos-debug` | macOS | Debug |
| `conda-macos-release` | macOS | Release |
| `conda-windows-debug` | Windows | Debug |
| `conda-windows-release` | Windows | Release |
All presets use the conda/pixi environment for dependency resolution.
## ccache
Enabled by default (`FREECAD_USE_CCACHE=ON`). CMake searches the system PATH for `ccache` at configure time and sets it as the compiler launcher:
```cmake
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
```
Disable with `-DFREECAD_USE_CCACHE=OFF` if needed. Clear the cache with `ccache -C`.
## Pixi tasks
Common build workflow:
```bash
pixi run initialize # git submodule update --init --recursive
pixi run configure # cmake --preset conda-linux-debug (or platform equivalent)
pixi run build # cmake --build build/debug
pixi run install # cmake --install build/debug
pixi run freecad # launch the built binary
pixi run test-kindred # run Kindred addon tests (no build needed)
```
Release variants: `configure-release`, `build-release`, `install-release`, `freecad-release`.
## Key CMake variables
| Variable | Default | Effect |
|----------|---------|--------|
| `BUILD_GUI` | ON | Build FreeCADGui at all |
| `FREECAD_USE_CCACHE` | ON | Compiler caching |
| `FREECAD_USE_PCH` | OFF (conda) | Precompiled headers |
| `FREECAD_USE_PYSIDE` | auto | PySide6 Python-Qt bridge |
| `FREECAD_USE_SHIBOKEN` | auto | Shiboken6 binding generator |
| `BUILD_VR` | OFF | Oculus VR support |
| `BUILD_TRACY_FRAME_PROFILER` | OFF | Tracy profiler integration |
## See also
- [Build System](./build-system.md) — general build instructions
- [Signal Architecture](../architecture/signal-architecture.md) — how fastsignals integrates at runtime
- [Creating a Custom Origin (C++)](../reference/cpp-custom-origin-guide.md) — what to link when adding an origin

View File

@@ -0,0 +1,73 @@
# Repository Structure
```
create/
├── src/
│ ├── App/ # Core application (C++)
│ ├── Base/ # Base classes (C++)
│ ├── Gui/ # GUI framework and stylesheets (C++)
│ ├── Main/ # Application entry points
│ ├── Mod/ # FreeCAD modules (~37)
│ │ ├── Create/ # Kindred bootstrap module
│ │ ├── Assembly/ # Assembly workbench (Kindred patches)
│ │ ├── PartDesign/ # Part Design workbench
│ │ ├── Sketcher/ # Sketcher workbench
│ │ ├── AddonManager/ # Addon manager (submodule)
│ │ └── ... # Other stock FreeCAD modules
│ └── 3rdParty/
│ ├── OndselSolver/ # Assembly solver (submodule)
│ └── GSL/ # Guidelines Support Library (submodule)
├── mods/ # Kindred addon workbenches (submodules)
│ ├── ztools/ # ztools workbench
│ └── silo/ # Silo parts database
├── kindred-icons/ # SVG icon library (~200 icons)
├── resources/ # Branding, desktop integration
│ ├── branding/ # Logo, splash, icon generation scripts
│ └── icons/ # Platform icons (.ico, .icns, hicolor)
├── package/ # Packaging scripts
│ ├── debian/ # Debian package
│ ├── ubuntu/ # Ubuntu-specific
│ ├── fedora/ # RPM package
│ ├── rattler-build/ # Cross-platform bundles (AppImage, DMG, NSIS)
│ └── WindowsInstaller/ # NSIS installer definition
├── .gitea/workflows/ # CI/CD pipelines
│ ├── build.yml # Build + test on push/PR
│ └── release.yml # Release on tag push
├── tests/ # Test suite
│ ├── src/ # C++ test sources
│ └── lib/ # Google Test framework (submodule)
├── cMake/ # CMake helper modules
├── docs/ # Documentation (this book)
├── tools/ # Dev utilities (build, lint, profile)
├── contrib/ # IDE configs (VSCode, CLion, debugger)
├── data/ # Example and test data
├── CMakeLists.txt # Root build configuration
├── CMakePresets.json # Platform build presets
├── pixi.toml # Pixi environment and tasks
├── CONTRIBUTING.md # Contribution guide
├── README.md # Project overview
├── LICENSE # LGPL-2.1-or-later
└── .pre-commit-config.yaml # Code quality hooks
```
## Git submodules
| Submodule | Path | Source | Purpose |
|-----------|------|--------|---------|
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | Unified workbench |
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | Parts database |
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | Assembly solver |
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | C++ guidelines library |
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | Extension manager |
| googletest | `tests/lib` | `github.com/google/googletest` | Test framework |
## Key files
| File | Purpose |
|------|---------|
| `src/Mod/Create/Init.py` | Console-phase bootstrap — loads addons |
| `src/Mod/Create/InitGui.py` | GUI-phase bootstrap — registers workbenches, deferred setup |
| `src/Gui/FileOrigin.h` | Abstract file origin interface (Kindred addition) |
| `src/Gui/Stylesheets/KindredCreate.qss` | Catppuccin Mocha theme |
| `pixi.toml` | Build tasks and dependencies |
| `CMakeLists.txt` | Root CMake configuration |

107
docs/src/guide/building.md Normal file
View File

@@ -0,0 +1,107 @@
# Building from Source
## Prerequisites
- **git** with submodule support
- **[pixi](https://pixi.sh)** — conda-based dependency manager and task runner
Pixi handles all other dependencies (CMake, compilers, Qt6, OpenCASCADE, etc.).
## Clone
```bash
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
cd create
```
If cloned without `--recursive`:
```bash
git submodule update --init --recursive
```
The repository includes six submodules:
| Submodule | Path | Source |
|-----------|------|--------|
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` |
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` |
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` |
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` |
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` |
| googletest | `tests/lib` | `github.com/google/googletest` |
## Build
```bash
pixi run configure
pixi run build
pixi run install
pixi run freecad
```
By default these target the **debug** configuration. For release builds:
```bash
pixi run configure-release
pixi run build-release
pixi run install-release
pixi run freecad-release
```
## All pixi tasks
| Task | Description |
|------|-------------|
| `initialize` | `git submodule update --init --recursive` |
| `configure` | CMake configure (debug) |
| `configure-debug` | CMake configure with debug preset |
| `configure-release` | CMake configure with release preset |
| `build` | Build (debug) |
| `build-debug` | `cmake --build build/debug` |
| `build-release` | `cmake --build build/release` |
| `install` | Install (debug) |
| `install-debug` | `cmake --install build/debug` |
| `install-release` | `cmake --install build/release` |
| `test` | Run tests (debug) |
| `test-debug` | `ctest --test-dir build/debug` |
| `test-release` | `ctest --test-dir build/release` |
| `freecad` | Launch FreeCAD (debug) |
| `freecad-debug` | `build/debug/bin/FreeCAD` |
| `freecad-release` | `build/release/bin/FreeCAD` |
## CMake presets
The project provides presets in `CMakePresets.json` for each platform and build type:
- `conda-linux-debug` / `conda-linux-release`
- `conda-macos-debug` / `conda-macos-release`
- `conda-windows-debug` / `conda-windows-release`
All presets inherit from a `common` base that enables `CMAKE_EXPORT_COMPILE_COMMANDS` and configures job pools. The `conda` presets use the Ninja generator and pick up compiler paths from the pixi environment.
## Platform notes
**Linux:** Uses clang from conda-forge. Requires kernel-headers, mesa, X11, and libspnav (all provided by pixi). Uses the mold linker for faster link times.
**macOS:** Minimal extra dependencies — pixi provides nearly everything. Tested on both Intel and Apple Silicon.
**Windows:** Requires pthreads-win32 and MSVC. The conda preset configures the MSVC toolchain automatically when run inside a pixi shell.
## Caching
The build uses **ccache** for compilation caching:
- Maximum cache size: 4 GB
- Compression: zlib level 6
- Sloppiness mode enabled for faster cache hits
ccache is auto-detected by CMake at configure time.
## Common problems
**Submodules not initialized:** If you see missing file errors for ztools or Silo, run `pixi run initialize` or `git submodule update --init --recursive`.
**Pixi not found:** Install pixi from <https://pixi.sh>.
**ccache full:** Clear with `ccache -C` or increase the limit in your ccache config.
**Preset not found:** Ensure you are running CMake from within a pixi shell (`pixi shell`) so that conda environment variables are set.

View File

@@ -0,0 +1,41 @@
# Getting Started
Kindred Create can be installed from prebuilt packages or built from source. This section covers both paths.
## Quick start (prebuilt)
Download the latest release from the [releases page](https://git.kindred-systems.com/kindred/create/releases).
**Debian/Ubuntu:**
```bash
sudo apt install ./kindred-create_*.deb
```
**AppImage:**
```bash
chmod +x KindredCreate-*.AppImage
./KindredCreate-*.AppImage
```
## Quick start (from source)
```bash
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
cd create
pixi run configure
pixi run build
pixi run freecad
```
See [Installation](./installation.md) for prebuilt package details and [Building from Source](./building.md) for the full development setup.
## First run
On first launch, Kindred Create:
1. Loads **ztools** commands and the **Silo** workbench via the Create bootstrap module
2. Opens the **PartDesign** workbench as the default (with context-driven toolbars)
3. Prompts for Silo server configuration if not yet set up
4. Checks for application updates in the background (after ~10 seconds)
If the Silo server is not available, Kindred Create operates normally with local file operations. The Silo features activate once a server is configured.

View File

@@ -0,0 +1,49 @@
# Installation
## Prebuilt packages
Download the latest release from the [releases page](https://git.kindred-systems.com/kindred/create/releases).
### Debian / Ubuntu
```bash
sudo apt install ./kindred-create_*.deb
```
### AppImage (any Linux)
```bash
chmod +x KindredCreate-*.AppImage
./KindredCreate-*.AppImage
```
The AppImage is a self-contained bundle using squashfs with zstd compression. No installation required.
### macOS
> macOS builds are planned but not yet available in CI. Build from source for now.
### Windows
> Windows builds are planned but not yet available in CI. Build from source for now.
## Verifying your installation
Launch Kindred Create and check the console output (View > Report View) for:
```
Create: Loaded ztools Init.py
Create: Loaded silo Init.py
Create module initialized
```
This confirms the bootstrap module loaded both workbenches. If Silo is not configured, you will see a settings prompt on first launch.
## Uninstalling
**Debian/Ubuntu:**
```bash
sudo apt remove kindred-create
```
**AppImage:** Delete the `.AppImage` file. No system files are modified.

148
docs/src/guide/silo.md Normal file
View File

@@ -0,0 +1,148 @@
# Silo
Silo is an item database and part management system for Kindred Create. It provides revision-controlled storage for CAD files, configurable part number generation, BOM management, and team collaboration.
- **Submodule path:** `mods/silo/`
- **Source:** `git.kindred-systems.com/kindred/silo-mod`
## Architecture
Silo has three components:
```
┌──────────────────────┐ ┌──────────────┐
│ FreeCAD Workbench │────▶│ Go REST API │
│ (Python commands) │ │ (silod) │
└──────────────────────┘ └──────┬───────┘
│ │
│ silo-client │
│ (shared API lib) │
│ ┌─────┴─────┐
│ │ │
│ PostgreSQL MinIO
│ (metadata) (files)
Local .FCStd files
```
- **Go REST API server** (`cmd/silod/`) — 38+ routes, backed by PostgreSQL and MinIO
- **FreeCAD workbench** (`freecad/`) — Python commands integrated into Kindred Create
- **Shared API client** (`silo-client/`) — Python library used by the workbench (nested submodule)
The silo-mod repository was split from a monorepo into three repos: `silo-client` (shared Python API client), `silo-mod` (FreeCAD workbench), and `silo-calc` (LibreOffice Calc extension).
## Workbench commands
### Document lifecycle
| Command | Shortcut | Description |
|---------|----------|-------------|
| `Silo_New` | Ctrl+N | Schema-driven item creation form — domain/subcategory picker, dynamic property fields loaded from the schema API, live part number preview, sourcing fields, and project tagging |
| `Silo_Open` | Ctrl+O | Search and open items — combined dialog querying both the database and local files |
| `Silo_Save` | Ctrl+S | Save locally to canonical path, collect document properties, upload to MinIO as auto-revision |
| `Silo_Commit` | Ctrl+Shift+S | Save as a new revision with a user-provided comment |
| `Silo_Pull` | — | Download from MinIO with revision selection, conflict detection, and progress tracking |
| `Silo_Push` | — | Batch upload — finds local files not yet synced to the server, compares timestamps |
### Information and management
| Command | Description |
|---------|-------------|
| `Silo_Info` | Show item metadata, project tags, and revision history table with status and labels |
| `Silo_BOM` | Two-tab view: BOM (children) and Where Used (parents). Add, edit, remove entries with quantity and unit tracking |
| `Silo_TagProjects` | Multi-select dialog for assigning project tags to items |
| `Silo_Rollback` | Select a previous revision and create a new revision from that point with optional comment |
| `Silo_SetStatus` | Change revision lifecycle status: draft → review → released → obsolete |
### Administration
| Command | Description |
|---------|-------------|
| `Silo_Settings` | Full settings UI — API URL, SSL verify, custom CA cert, API token management, authentication status |
| `Silo_Auth` | Session-based login: `/login``/api/auth/me``/api/auth/tokens`; stores API token in preferences |
| `Silo_ToggleMode` | Switch between Silo workbench and other workbenches (menu only) |
## Origin integration
Silo registers as a **file origin** via the `FileOrigin` interface in `src/Gui/`. The `SiloOrigin` class in `silo_origin.py` implements:
| Capability | Value |
|------------|-------|
| `id` | `"silo"` |
| `name` | `"Kindred Silo"` |
| `type` | PLM (1) |
| `tracksExternally` | true |
| `requiresAuthentication` | true |
| `supportsRevisions` | true |
| `supportsBOM` | true |
| `supportsPartNumbers` | true |
| `supportsAssemblies` | true |
The origin delegates to the workbench commands for all operations (new, open, save, commit, pull, push, info, BOM). Registration happens via a deferred QTimer (1500ms after startup) in `src/Mod/Create/InitGui.py`.
## Configuration
### FreeCAD parameters
Stored in `User parameter:BaseApp/Preferences/Mod/KindredSilo`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ApiUrl` | String | (empty) | Silo server URL |
| `SslVerify` | Bool | true | Verify SSL certificates |
| `CaCertPath` | String | (empty) | Path to custom CA certificate |
| `ApiToken` | String | (empty) | Stored authentication token |
| `FirstStartChecked` | Bool | false | Whether first-start prompt has been shown |
| `ProjectsDir` | String | `~/projects` | Local directory for checked-out files |
### Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `SILO_API_URL` | `http://localhost:8080/api` | Override for server API endpoint |
| `SILO_PROJECTS_DIR` | `~/projects` | Override for local projects directory |
## Server
The Silo server is documented in detail in the [Silo Server](../silo-server/overview.md) section:
- [Configuration](../silo-server/CONFIGURATION.md) — YAML config, database, MinIO, auth settings
- [Deployment](../silo-server/DEPLOYMENT.md) — Docker Compose, systemd, production setup
- [Specification](../silo-server/SPECIFICATION.md) — Full API specification with 38+ routes
- [Authentication](../silo-server/AUTH.md) — LDAP, OIDC, and local auth backends
## Directory structure
```
mods/silo/
├── cmd/
│ ├── silo/ # CLI tool
│ └── silod/ # API server
├── internal/
│ ├── api/ # HTTP handlers, routes, templates
│ ├── config/ # Configuration loading
│ ├── db/ # PostgreSQL access
│ ├── migration/ # Property migration utilities
│ ├── partnum/ # Part number generation
│ ├── schema/ # YAML schema parsing
│ └── storage/ # MinIO file storage
├── freecad/
│ ├── InitGui.py # SiloWorkbench registration
│ ├── schema_form.py # Schema-driven item creation dialog (SchemaFormDialog)
│ ├── silo_commands.py # 14 commands + dock widgets
│ ├── silo_origin.py # FileOrigin backend
│ ├── silo_start.py # Native start panel (database items, activity feed)
│ └── resources/icons/ # 10 silo-*.svg icons
├── silo-client/ # Shared Python API client (nested submodule)
│ └── silo_client/
│ ├── client.py # SiloClient HTTP wrapper
│ └── settings.py # SiloSettings config management
├── migrations/ # 10 numbered SQL scripts
├── schemas/ # Part numbering YAML schemas
└── deployments/ # Docker Compose + systemd configs
```
## Further reading
- `mods/silo/README.md` — server quickstart and CLI usage
- `mods/silo/ROADMAP.md` — strategic roadmap (6 phases, Q2 2026 → Q4 2027)

View File

@@ -0,0 +1,23 @@
# Workbenches
Kindred Create ships two custom workbenches on top of FreeCAD's standard set.
## ztools
A unified workbench that consolidates part design, assembly, and sketcher tools into a single interface. It is the **default workbench** when Kindred Create launches.
ztools commands are also injected into the PartDesign workbench menus and toolbars via a manipulator mechanism, so they are accessible even when working in stock PartDesign.
See the [ztools guide](./ztools.md) for details.
## Silo
A parts database workbench for managing CAD files, part numbers, revisions, and bills of materials across teams. Silo commands (New, Open, Save, Commit, Pull, Push, Info, BOM) are integrated into the File menu and toolbar across **all** workbenches via the origin system.
Silo requires a running server instance. On first launch, Kindred Create prompts for server configuration.
See the [Silo guide](./silo.md) for details.
## Stock FreeCAD workbenches
All standard FreeCAD workbenches are available: PartDesign, Sketcher, Assembly, TechDraw, Draft, BIM, CAM, FEM, Mesh, Spreadsheet, and others. Kindred Create does not remove or disable any stock functionality.

133
docs/src/guide/ztools.md Normal file
View File

@@ -0,0 +1,133 @@
# ztools
ztools is a pure-Python FreeCAD workbench that consolidates part design, assembly, and sketcher tools into a single unified interface. It is the **default workbench** when Kindred Create launches.
- **Submodule path:** `mods/ztools/`
- **Source:** `git.kindred-systems.com/forbes/ztools`
- **Stats:** 6,400+ lines of code, 24+ command classes, 33 custom icons, 17 toolbars
## Commands
### Datum Creator (`ZTools_DatumCreator`)
Creates datum geometry (planes, axes, points) with 16 creation modes. The task panel auto-detects the geometry type from your selection and offers appropriate modes. Supports custom naming, spreadsheet linking, and body- or document-level creation.
### Datum Manager (`ZTools_DatumManager`)
Manages existing datums. (Stub — planned for Phase 1, Q1 2026.)
### Enhanced Pocket (`ZTools_EnhancedPocket`)
Extends FreeCAD's Pocket feature with **Flip Side to Cut** — a SOLIDWORKS-style feature that removes material *outside* the sketch profile rather than inside. Uses a Boolean Common operation internally. Supports all standard pocket types: Dimension, Through All, To First, Up To Face, Two Dimensions. Taper angle is supported for standard pockets (disabled for flipped).
### Rotated Linear Pattern (`ZTools_RotatedLinearPattern`)
Creates a linear pattern with incremental rotation per instance. Configure direction, spacing, number of occurrences, and cumulative or per-instance rotation. Source components are automatically hidden.
### Assembly Linear Pattern (`ZTools_AssemblyLinearPattern`)
Creates linear patterns of assembly components. Supports multi-component selection, direction vectors, total length or spacing modes, and creation as Links (recommended) or copies. Auto-detects the parent assembly.
### Assembly Polar Pattern (`ZTools_AssemblyPolarPattern`)
Creates polar (circular) patterns of assembly components. Supports custom or preset axes (X/Y/Z), full circle or custom angle, center point definition, and creation as Links or copies.
### Spreadsheet Formatting (9 commands)
| Command | Action |
|---------|--------|
| `ZTools_SpreadsheetStyleBold` | Toggle bold |
| `ZTools_SpreadsheetStyleItalic` | Toggle italic |
| `ZTools_SpreadsheetStyleUnderline` | Toggle underline |
| `ZTools_SpreadsheetAlignLeft` | Left align |
| `ZTools_SpreadsheetAlignCenter` | Center align |
| `ZTools_SpreadsheetAlignRight` | Right align |
| `ZTools_SpreadsheetBgColor` | Background color picker |
| `ZTools_SpreadsheetTextColor` | Text color picker |
| `ZTools_SpreadsheetQuickAlias` | Auto-create aliases from row/column labels |
## Datum creation modes
### Planes (7 modes)
| Mode | Description | Input |
|------|-------------|-------|
| Offset from Face | Offsets a planar face along its normal | Face + distance (mm) |
| Offset from Plane | Offsets an existing datum plane | Datum plane + distance (mm) |
| Midplane | Plane halfway between two parallel faces | Two parallel faces |
| 3 Points | Plane through three non-collinear points | Three vertices |
| Normal to Edge | Plane perpendicular to an edge at a parameter location | Edge + parameter (0.01.0) |
| Angled | Rotates a plane about an edge by a specified angle | Face + edge + angle (degrees) |
| Tangent to Cylinder | Plane tangent to a cylindrical face at an angular position | Cylindrical face + angle (degrees) |
### Axes (4 modes)
| Mode | Description | Input |
|------|-------------|-------|
| 2 Points | Axis through two points | Two vertices |
| From Edge | Axis along a linear edge | Linear edge |
| Cylinder Center | Axis along the centerline of a cylinder | Cylindrical face |
| Plane Intersection | Axis at the intersection of two planes | Two non-parallel planes |
### Points (5 modes)
| Mode | Description | Input |
|------|-------------|-------|
| At Vertex | Point at a vertex location | Vertex |
| XYZ Coordinates | Point at explicit coordinates | x, y, z (mm) |
| On Edge | Point at a location along an edge | Edge + parameter (0.01.0) |
| Face Center | Point at the center of mass of a face | Face |
| Circle Center | Point at the center of a circular or arc edge | Circular edge |
## PartDesign injection
ztools registers a `_ZToolsPartDesignManipulator` that hooks into the PartDesign workbench at startup. This injects the following commands into PartDesign's toolbars and menus:
| PartDesign toolbar | Injected command |
|--------------------|-----------------|
| Part Design Helper Features | `ZTools_DatumCreator`, `ZTools_DatumManager` |
| Part Design Modeling Features | `ZTools_EnhancedPocket` |
| Part Design Transformation Features | `ZTools_RotatedLinearPattern` |
The manipulator is registered in `InitGui.py` when the Create bootstrap module loads addon workbenches.
## Directory structure
```
mods/ztools/
├── ztools/ztools/
│ ├── InitGui.py # Workbench registration + manipulator
│ ├── Init.py # Console initialization
│ ├── commands/
│ │ ├── datum_commands.py # DatumCreator + DatumManager
│ │ ├── datum_viewprovider.py # ViewProvider + edit panel
│ │ ├── pocket_commands.py # EnhancedPocket + FlippedPocket
│ │ ├── pattern_commands.py # RotatedLinearPattern
│ │ ├── assembly_pattern_commands.py # Linear + Polar assembly patterns
│ │ └── spreadsheet_commands.py # 9 formatting commands
│ ├── datums/
│ │ └── core.py # 16 datum creation functions
│ └── resources/ # Icons and theme
└── CatppuccinMocha/ # Theme preference pack
```
## Internal properties
ztools stores metadata on feature objects using these properties (preserved for backward compatibility):
| Property | Purpose |
|----------|---------|
| `ZTools_Type` | Feature type identifier |
| `ZTools_Params` | JSON creation parameters |
| `ZTools_SourceRefs` | JSON source geometry references |
## Known gaps
- Datum Manager is a stub — full implementation planned for Q1 2026
- Datum parameter changes don't recalculate from source geometry yet
- Enhanced Pocket taper angle is disabled for flipped pockets
## Further reading
- `mods/ztools/KINDRED_INTEGRATION.md` — integration architecture and migration options
- `mods/ztools/ROADMAP.md` — phased development plan (Q1Q4 2026)

37
docs/src/introduction.md Normal file
View File

@@ -0,0 +1,37 @@
# Kindred Create
Kindred Create is a fork of [FreeCAD](https://www.freecad.org) 1.0+ that adds integrated tooling for professional engineering workflows. It ships custom workbenches and a dark theme on top of FreeCAD's parametric modeling core.
- **License:** LGPL 2.1+
- **Organization:** [Kindred Systems LLC](https://www.kindred-systems.com)
- **Build system:** CMake + [pixi](https://pixi.sh)
- **Current version:** Kindred Create v0.1.0 (FreeCAD base v1.0.0)
## Key features
**[ztools](./guide/ztools.md)** — A unified workbench that consolidates part design, assembly, and sketcher tools into a single interface. Adds custom datum creation (planes, axes, points with 16 creation modes), pattern tools for assemblies, an enhanced pocket with flip-side cutting, and spreadsheet formatting commands.
**[Silo](./guide/silo.md)** — A parts database system for managing CAD files, part numbers, revisions, and bills of materials across teams. Includes a Go REST API server backed by PostgreSQL and MinIO, with FreeCAD commands for opening, saving, and syncing files directly from the application.
**Catppuccin Mocha theme** — A dark theme applied across the entire application, including the 3D viewport, sketch editor, spreadsheet view, and tree view. Uses spanning-line branch indicators instead of disclosure arrows, with tuned preference defaults for document handling, selection behavior, and notifications.
**Origin system** — A pluggable file backend abstraction. The origin selector in the File toolbar lets you switch between local filesystem operations and Silo database operations. Silo commands (Commit, Pull, Push, Info, BOM) are available across all workbenches when Silo is the active origin.
**Update checker** — On startup, Kindred Create checks the Gitea releases API for newer versions and logs the result. Configurable check interval and skip-version preferences.
## How it relates to FreeCAD
Kindred Create is a fork/distribution of FreeCAD 1.0+. The design minimizes core modifications — custom functionality is delivered through submodule addons (ztools, Silo) that follow FreeCAD's standard workbench pattern. If the addon submodules are missing, Kindred Create still functions as a themed FreeCAD.
The primary additions to FreeCAD's core are:
- The **origin system** (`FileOrigin` interface in `src/Gui/`) for pluggable file backends
- The **Create bootstrap module** (`src/Mod/Create/`) that loads addons at startup
- The **Catppuccin Mocha theme** (`KindredCreate.qss`) and preference pack
- Patches to **Assembly** (`findPlacement()` datum/origin handling)
## Links
- **Source:** <https://git.kindred-systems.com/kindred/create>
- **Downloads:** <https://git.kindred-systems.com/kindred/create/releases>
- **Issue tracker:** <https://git.kindred-systems.com/kindred/create/issues>
- **Website:** <https://www.kindred-systems.com/create>

View File

@@ -0,0 +1,110 @@
# Configuration
## Silo workbench
### FreeCAD parameters
Stored in `User parameter:BaseApp/Preferences/Mod/KindredSilo`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ApiUrl` | String | (empty) | Silo server API endpoint URL |
| `SslVerify` | Bool | true | Verify SSL certificates when connecting to server |
| `CaCertPath` | String | (empty) | Path to custom CA certificate for self-signed certs |
| `ApiToken` | String | (empty) | Stored authentication token (set by `Silo_Auth`) |
| `FirstStartChecked` | Bool | false | Whether the first-start settings prompt has been shown |
| `ProjectsDir` | String | `~/projects` | Local directory for checked-out CAD files |
### Environment variables
These override the FreeCAD parameter values when set:
| Variable | Default | Description |
|----------|---------|-------------|
| `SILO_API_URL` | `http://localhost:8080/api` | Silo server API endpoint |
| `SILO_PROJECTS_DIR` | `~/projects` | Local directory for checked-out files |
### Keyboard shortcuts
Recommended shortcuts (prompted on first workbench activation):
| Shortcut | Command |
|----------|---------|
| Ctrl+O | `Silo_Open` — Search and open items |
| Ctrl+N | `Silo_New` — Schema-driven item creation form |
| Ctrl+S | `Silo_Save` — Save locally and upload |
| Ctrl+Shift+S | `Silo_Commit` — Save with revision comment |
## Update checker
Stored in `User parameter:BaseApp/Preferences/Mod/KindredCreate/Update`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `CheckEnabled` | Bool | true | Enable or disable update checks |
| `CheckIntervalDays` | Int | 1 | Minimum days between checks |
| `LastCheckTimestamp` | String | (empty) | ISO 8601 timestamp of last successful check |
| `SkippedVersion` | String | (empty) | Version the user chose to skip |
The checker queries:
```
https://git.kindred-systems.com/api/v1/repos/kindred/create/releases?limit=10
```
It compares the current version (injected at build time via `version.py.in`) against the latest non-draft, non-prerelease tag. The `latest` rolling tag is ignored. Checks run 10 seconds after GUI startup.
To disable: set `CheckEnabled` to `false` in FreeCAD preferences, or set `CheckIntervalDays` to `0` for on-demand only.
## Theme
The default theme is **Catppuccin Mocha** applied via `KindredCreate.qss`.
| Setting | Location |
|---------|----------|
| Canonical stylesheet | `src/Gui/Stylesheets/KindredCreate.qss` |
| Preference pack | `src/Gui/PreferencePacks/KindredCreate/` |
| Default theme name | `coal` (in mdBook docs) / `KindredCreate` (in app) |
To switch themes: **Edit > Preferences > General > Stylesheet** and select a different `.qss` file.
The preference pack is synced from the canonical stylesheet at build time via CMake's `configure_file()`. Edits should be made to the canonical file, not the preference pack copy.
## Build configuration
### Version constants
Defined in the root `CMakeLists.txt`:
| Constant | Value | Description |
|----------|-------|-------------|
| `KINDRED_CREATE_VERSION` | `0.1.0` | Kindred Create version |
| `FREECAD_VERSION` | `1.0.0` | FreeCAD base version |
These are injected into `src/Mod/Create/version.py` at build time via `version.py.in`.
### CMake presets
Defined in `CMakePresets.json`:
| Preset | Platform | Build type |
|--------|----------|------------|
| `conda-linux-debug` | Linux | Debug |
| `conda-linux-release` | Linux | Release |
| `conda-macos-debug` | macOS | Debug |
| `conda-macos-release` | macOS | Release |
| `conda-windows-debug` | Windows | Debug |
| `conda-windows-release` | Windows | Release |
### ccache
| Setting | Value |
|---------|-------|
| Max size | 4 GB |
| Compression | zlib level 6 |
| Sloppiness | `include_file_ctime,include_file_mtime,pch_defines,time_macros` |
ccache is auto-detected by CMake at configure time. Clear with `ccache -C`.
## Silo server
Server configuration is documented in the dedicated [Silo Server Configuration](../silo-server/CONFIGURATION.md) reference, which covers all YAML config sections (database, storage, auth, server, schemas) with full option tables and examples.

View File

@@ -0,0 +1,142 @@
# CommandOrigin — File Menu Origin Commands
> **Source:** `src/Gui/CommandOrigin.cpp`
> **Namespace:** `Gui`
> **Registration:** `Gui::CreateOriginCommands()`
Five FreeCAD commands expose the extended operations defined by
[FileOrigin](./cpp-file-origin.md). Each command follows the same
pattern: look up the owning origin for the active document via
[OriginManager](./cpp-origin-manager.md), check whether that origin
advertises the required capability, and dispatch to the corresponding
`FileOrigin` virtual method.
## Command Table
| Command ID | Menu Text | Shortcut | Capability Gate | Dispatches To | Icon |
|-----------|-----------|----------|-----------------|---------------|------|
| `Origin_Commit` | &Commit | `Ctrl+Shift+C` | `supportsRevisions()` | `commitDocument(doc)` | `silo-commit` |
| `Origin_Pull` | &Pull | `Ctrl+Shift+P` | `supportsRevisions()` | `pullDocument(doc)` | `silo-pull` |
| `Origin_Push` | Pu&sh | `Ctrl+Shift+U` | `supportsRevisions()` | `pushDocument(doc)` | `silo-push` |
| `Origin_Info` | &Info | — | `supportsPartNumbers()` | `showInfo(doc)` | `silo-info` |
| `Origin_BOM` | &Bill of Materials | — | `supportsBOM()` | `showBOM(doc)` | `silo-bom` |
All commands belong to the `"File"` group.
## Activation Pattern
Every command shares the same `isActive()` / `activated()` structure:
```
isActive():
doc = App::GetApplication().getActiveDocument()
if doc is null → return false
origin = OriginManager::instance()->findOwningOrigin(doc)
return origin != null AND origin->supportsXxx()
activated():
doc = App::GetApplication().getActiveDocument()
if doc is null → return
origin = OriginManager::instance()->findOwningOrigin(doc)
if origin AND origin->supportsXxx():
origin->xxxDocument(doc)
```
This means the commands **automatically grey out** in the menu when:
- No document is open.
- The active document is owned by an origin that doesn't support the
capability (e.g., a local document has no Commit/Pull/Push).
For a local-only document, all five commands are inactive. When a Silo
document is active, all five become available because
[SiloOrigin](../reference/cpp-file-origin.md) returns `true` for
`supportsRevisions()`, `supportsBOM()`, and `supportsPartNumbers()`.
## Ownership Resolution
The commands use `findOwningOrigin(doc)` rather than `currentOrigin()`
because the active document may belong to a **different** origin than the
one currently selected in the toolbar. For example, a user might have
Silo selected as the current origin but be viewing a local document in
another tab — the commands correctly detect that the local document has
no revision support.
See [OriginManager § Document-Origin Resolution](./cpp-origin-manager.md#document-origin-resolution)
for the full lookup algorithm.
## Command Type Flags
| Command | `eType` | Meaning |
|---------|---------|---------|
| Commit | `AlterDoc` | Marks document as modified (undo integration) |
| Pull | `AlterDoc` | Marks document as modified |
| Push | `AlterDoc` | Marks document as modified |
| Info | `0` | Read-only, no undo integration |
| BOM | `0` | Read-only, no undo integration |
`AlterDoc` commands participate in FreeCAD's transaction/undo system.
Info and BOM are view-only dialogs that don't modify the document.
## Registration
All five commands are registered in a single function called during
application startup:
```cpp
void Gui::CreateOriginCommands()
{
CommandManager& rcCmdMgr = Application::Instance->commandManager();
rcCmdMgr.addCommand(new OriginCmdCommit());
rcCmdMgr.addCommand(new OriginCmdPull());
rcCmdMgr.addCommand(new OriginCmdPush());
rcCmdMgr.addCommand(new OriginCmdInfo());
rcCmdMgr.addCommand(new OriginCmdBOM());
}
```
This is called from the Gui module initialization alongside other
command registration functions (`CreateDocCommands`,
`CreateMacroCommands`, etc.).
## What the Commands Actually Do
The C++ commands are **thin dispatchers** — they contain no business
logic. The actual work happens in the origin implementation:
- **Local origin** — all five extended methods are no-ops (defaults from
`FileOrigin` base class return `false` or do nothing).
- **Silo origin** (Python) — each method delegates to the corresponding
`Silo_*` FreeCAD command:
| C++ dispatch | Python SiloOrigin method | Delegates to |
|-------------|-------------------------|--------------|
| `commitDocument()` | `SiloOrigin.commitDocument()` | `Silo_Commit` command |
| `pullDocument()` | `SiloOrigin.pullDocument()` | `Silo_Pull` command |
| `pushDocument()` | `SiloOrigin.pushDocument()` | `Silo_Push` command |
| `showInfo()` | `SiloOrigin.showInfo()` | `Silo_Info` command |
| `showBOM()` | `SiloOrigin.showBOM()` | `Silo_BOM` command |
The call chain for a Commit, for example:
```
User clicks File > Commit (or Ctrl+Shift+C)
→ OriginCmdCommit::activated()
→ OriginManager::findOwningOrigin(doc) [C++]
→ SiloOrigin.ownsDocument(doc) [Python via bridge]
→ origin->commitDocument(doc) [C++ virtual]
→ FileOriginPython::commitDocument(doc) [bridge]
→ SiloOrigin.commitDocument(doc) [Python]
→ FreeCADGui.runCommand("Silo_Commit") [Python command]
```
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — defines the virtual
methods these commands dispatch to
- [OriginManager](./cpp-origin-manager.md) — provides
`findOwningOrigin()` used by every command
- [FileOriginPython](./cpp-file-origin-python.md) — bridges the dispatch
from C++ to Python origins
- [SiloOrigin adapter](./cpp-file-origin.md) — Python implementation
that handles the actual Silo operations

View File

@@ -0,0 +1,537 @@
# Creating a Custom Origin in C++
This guide walks through implementing and registering a new `FileOrigin` backend in C++. By the end you will have a working origin that appears in the toolbar selector, responds to File menu commands, and integrates with the command dispatch system.
For the Python equivalent, see [Creating a Custom Origin in Python](./python-custom-origin-guide.md).
## Prerequisites
Familiarity with:
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract base class
- [OriginManager](./cpp-origin-manager.md) — registration and document resolution
- [CommandOrigin](./cpp-command-origin.md) — how commands dispatch to origins
## What an origin does
An origin defines **how documents are tracked and stored**. It answers:
- Does this document belong to me? (`ownsDocument`)
- What is this document's identity? (`documentIdentity`)
- How do I create, open, and save documents? (6 document operations)
- What extended capabilities do I support? (revisions, BOM, part numbers)
Once registered, the origin appears in the toolbar selector and all File/Origin commands automatically dispatch to it for documents it owns.
## Step 1: Choose your origin type
```cpp
enum class OriginType { Local, PLM, Cloud, Custom };
```
| Type | Use when | `tracksExternally` | `requiresAuthentication` |
|------|----------|-------------------|-------------------------|
| `Local` | Filesystem storage, no sync | `false` | `false` |
| `PLM` | Database-backed with revisions (Silo, Teamcenter, Windchill) | `true` | usually `true` |
| `Cloud` | Remote file storage (S3, OneDrive, Google Drive) | `true` | usually `true` |
| `Custom` | Anything else | your choice | your choice |
## Step 2: Define the class
Create a header and source file in `src/Gui/`:
```cpp
// src/Gui/MyOrigin.h
#ifndef GUI_MY_ORIGIN_H
#define GUI_MY_ORIGIN_H
#include "FileOrigin.h"
namespace Gui {
class MyOrigin : public FileOrigin
{
public:
MyOrigin();
~MyOrigin() override = default;
// --- Identity (required) ---
std::string id() const override;
std::string name() const override;
std::string nickname() const override;
QIcon icon() const override;
OriginType type() const override;
// --- Ownership (required) ---
bool ownsDocument(App::Document* doc) const override;
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
// --- Document operations (required) ---
App::Document* newDocument(const std::string& name = "") override;
App::Document* openDocument(const std::string& identity) override;
App::Document* openDocumentInteractive() override;
bool saveDocument(App::Document* doc) override;
bool saveDocumentAs(App::Document* doc,
const std::string& newIdentity) override;
bool saveDocumentAsInteractive(App::Document* doc) override;
// --- Characteristics (override defaults) ---
bool tracksExternally() const override;
bool requiresAuthentication() const override;
// --- Capabilities (override if supported) ---
bool supportsRevisions() const override;
bool supportsBOM() const override;
bool supportsPartNumbers() const override;
// --- Extended operations (implement if capabilities are true) ---
bool commitDocument(App::Document* doc) override;
bool pullDocument(App::Document* doc) override;
bool pushDocument(App::Document* doc) override;
void showInfo(App::Document* doc) override;
void showBOM(App::Document* doc) override;
// --- Connection (override for authenticated origins) ---
ConnectionState connectionState() const override;
bool connect() override;
void disconnect() override;
};
} // namespace Gui
#endif
```
## Step 3: Implement identity methods
Every origin needs a unique `id()` (lowercase, alphanumeric with hyphens), a human-readable `name()`, a short `nickname()` for toolbar display, an `icon()`, and a `type()`.
```cpp
std::string MyOrigin::id() const { return "my-plm"; }
std::string MyOrigin::name() const { return "My PLM System"; }
std::string MyOrigin::nickname() const { return "My PLM"; }
OriginType MyOrigin::type() const { return OriginType::PLM; }
QIcon MyOrigin::icon() const
{
return BitmapFactory().iconFromTheme("server-database");
}
```
The `nickname()` appears in the toolbar button (keep it under ~15 characters). The full `name()` appears in tooltips.
## Step 4: Implement ownership detection
`ownsDocument()` is called by `OriginManager::findOwningOrigin()` to determine which origin owns a given document. The standard pattern is to check for a **tracking property** on objects in the document.
```cpp
static const char* MY_TRACKING_PROP = "MyPlmItemId";
bool MyOrigin::ownsDocument(App::Document* doc) const
{
if (!doc) {
return false;
}
for (auto* obj : doc->getObjects()) {
if (obj->getPropertyByName(MY_TRACKING_PROP)) {
return true;
}
}
return false;
}
```
**Key rules:**
- Return `true` only for documents that have your tracking marker.
- The built-in `LocalFileOrigin` claims documents by *exclusion* — it owns anything that no other origin claims. Your origin must positively identify its documents.
- OriginManager checks non-local origins first, then falls back to local. Your `ownsDocument()` takes priority.
- Keep the check fast — it runs on every document open and on command activation queries.
### Setting the tracking property
When your origin creates or first saves a document, add the tracking property:
```cpp
void MyOrigin::markDocument(App::Document* doc, const std::string& itemId)
{
// Add to the first object, or create a marker object
auto* obj = doc->getObjects().empty()
? doc->addObject("App::DocumentObject", "MyPlm_Marker")
: doc->getObjects().front();
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName(MY_TRACKING_PROP));
if (!prop) {
prop = static_cast<App::PropertyString*>(
obj->addDynamicProperty("App::PropertyString", MY_TRACKING_PROP));
}
prop->setValue(itemId.c_str());
}
```
## Step 5: Implement document identity
Two methods distinguish machine identity from display identity:
| Method | Purpose | Example (local) | Example (PLM) |
|--------|---------|-----------------|----------------|
| `documentIdentity()` | Immutable tracking key | `/home/user/part.FCStd` | `550e8400-...` (UUID) |
| `documentDisplayId()` | Human-readable label | `/home/user/part.FCStd` | `WHEEL-001-RevC` |
`documentIdentity()` must be stable — the same document must always produce the same identity. `openDocument(identity)` must be able to reopen the document from this value.
```cpp
std::string MyOrigin::documentIdentity(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
for (auto* obj : doc->getObjects()) {
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName(MY_TRACKING_PROP));
if (prop) {
return prop->getValue();
}
}
return {};
}
std::string MyOrigin::documentDisplayId(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
// Show a friendly part number instead of UUID
for (auto* obj : doc->getObjects()) {
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName("PartNumber"));
if (prop && prop->getValue()[0] != '\0') {
return prop->getValue();
}
}
// Fall back to identity
return documentIdentity(doc);
}
```
## Step 6: Implement document operations
All six document operations are pure virtual and must be implemented.
### newDocument
Called when the user creates a new document while your origin is active.
```cpp
App::Document* MyOrigin::newDocument(const std::string& name)
{
// For a PLM origin, you might show a part creation dialog first
MyPartDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
return nullptr;
}
std::string docName = name.empty() ? "Unnamed" : name;
App::Document* doc = App::GetApplication().newDocument(docName.c_str());
// Mark the document as ours
markDocument(doc, dlg.generatedUUID());
return doc;
}
```
### openDocument / openDocumentInteractive
```cpp
App::Document* MyOrigin::openDocument(const std::string& identity)
{
if (identity.empty()) {
return nullptr;
}
// Download file from backend if not cached locally
std::string localPath = fetchFromBackend(identity);
if (localPath.empty()) {
return nullptr;
}
return App::GetApplication().openDocument(localPath.c_str());
}
App::Document* MyOrigin::openDocumentInteractive()
{
// Show a search/browse dialog
MySearchDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
return nullptr;
}
return openDocument(dlg.selectedIdentity());
}
```
### saveDocument / saveDocumentAs / saveDocumentAsInteractive
```cpp
bool MyOrigin::saveDocument(App::Document* doc)
{
if (!doc) {
return false;
}
const char* path = doc->FileName.getValue();
if (!path || path[0] == '\0') {
return false; // No path yet — caller will use saveDocumentAsInteractive
}
if (!doc->save()) {
return false;
}
// Sync metadata to backend
return syncToBackend(doc);
}
bool MyOrigin::saveDocumentAs(App::Document* doc,
const std::string& newIdentity)
{
if (!doc || newIdentity.empty()) {
return false;
}
std::string localPath = localPathForIdentity(newIdentity);
if (!doc->saveAs(localPath.c_str())) {
return false;
}
markDocument(doc, newIdentity);
return syncToBackend(doc);
}
bool MyOrigin::saveDocumentAsInteractive(App::Document* doc)
{
if (!doc) {
return false;
}
Gui::Document* guiDoc = Application::Instance->getDocument(doc);
if (!guiDoc) {
return false;
}
return guiDoc->saveAs();
}
```
**Save workflow:** When the user presses Ctrl+S, the command layer calls `saveDocument()`. If it returns `false` (no path set), the command layer automatically falls through to `saveDocumentAsInteractive()`.
## Step 7: Implement capabilities and extended operations
Override capability flags to enable the corresponding toolbar commands:
```cpp
bool MyOrigin::supportsRevisions() const { return true; }
bool MyOrigin::supportsBOM() const { return true; }
bool MyOrigin::supportsPartNumbers() const { return true; }
```
Then implement the operations they gate:
| Flag | Enables commands | Methods to implement |
|------|-----------------|---------------------|
| `supportsRevisions()` | Origin_Commit, Origin_Pull, Origin_Push | `commitDocument`, `pullDocument`, `pushDocument` |
| `supportsBOM()` | Origin_BOM | `showBOM` |
| `supportsPartNumbers()` | Origin_Info | `showInfo` |
```cpp
bool MyOrigin::commitDocument(App::Document* doc)
{
if (!doc) return false;
// Show commit dialog, upload revision
MyCommitDialog dlg(getMainWindow(), doc);
if (dlg.exec() != QDialog::Accepted) return false;
return uploadRevision(documentIdentity(doc), dlg.message());
}
bool MyOrigin::pullDocument(App::Document* doc)
{
if (!doc) return false;
// Show revision picker, download selected revision
MyRevisionDialog dlg(getMainWindow(), documentIdentity(doc));
if (dlg.exec() != QDialog::Accepted) return false;
return downloadRevision(doc, dlg.selectedRevisionId());
}
bool MyOrigin::pushDocument(App::Document* doc)
{
if (!doc) return false;
if (!doc->save()) return false;
return uploadCurrentState(documentIdentity(doc));
}
void MyOrigin::showInfo(App::Document* doc)
{
if (!doc) return;
MyInfoDialog dlg(getMainWindow(), doc);
dlg.exec();
}
void MyOrigin::showBOM(App::Document* doc)
{
if (!doc) return;
MyBOMDialog dlg(getMainWindow(), doc);
dlg.exec();
}
```
Commands that are not supported simply remain at their base-class defaults (`return false` / no-op). The toolbar buttons for unsupported commands are automatically greyed out.
### How command dispatch works
When the user clicks Origin_Commit:
1. `OriginCmdCommit::isActive()` checks `origin->supportsRevisions()` — if `false`, the button is greyed out.
2. `OriginCmdCommit::activated()` calls `OriginManager::instance()->findOwningOrigin(doc)` to get the origin for the active document.
3. It then calls `origin->commitDocument(doc)`.
4. Your implementation runs.
No routing code is needed — the command system handles dispatch automatically based on document ownership.
## Step 8: Implement connection lifecycle (authenticated origins)
If your origin requires authentication, override the connection methods:
```cpp
bool MyOrigin::requiresAuthentication() const { return true; }
ConnectionState MyOrigin::connectionState() const
{
return m_connectionState; // private member
}
bool MyOrigin::connect()
{
m_connectionState = ConnectionState::Connecting;
signalConnectionStateChanged(ConnectionState::Connecting);
// Show login dialog or attempt token-based auth
MyLoginDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
m_connectionState = ConnectionState::Disconnected;
signalConnectionStateChanged(ConnectionState::Disconnected);
return false;
}
if (!authenticateWithServer(dlg.credentials())) {
m_connectionState = ConnectionState::Error;
signalConnectionStateChanged(ConnectionState::Error);
return false;
}
m_connectionState = ConnectionState::Connected;
signalConnectionStateChanged(ConnectionState::Connected);
return true;
}
void MyOrigin::disconnect()
{
invalidateSession();
m_connectionState = ConnectionState::Disconnected;
signalConnectionStateChanged(ConnectionState::Disconnected);
}
```
**Connection state lifecycle:**
```
Disconnected ──connect()──→ Connecting ──success──→ Connected
──failure──→ Error
Connected ──disconnect()──→ Disconnected
Error ──connect()──→ Connecting ──...
```
The `OriginSelectorWidget` listens to `signalConnectionStateChanged` and:
- Adds a red overlay for `Disconnected`
- Adds a yellow overlay for `Error`
- Shows no overlay for `Connected`
When the user selects a disconnected origin in the toolbar, the widget calls `connect()` automatically. If `connect()` returns `false`, the selection reverts to the previous origin.
## Step 9: Register with OriginManager
Call `registerOrigin()` during module initialisation:
```cpp
#include "OriginManager.h"
#include "MyOrigin.h"
void initMyOriginModule()
{
auto* origin = new MyOrigin();
if (!OriginManager::instance()->registerOrigin(origin)) {
Base::Console().error("Failed to register MyOrigin\n");
// origin is deleted by OriginManager on failure
}
}
```
**Registration rules:**
- `id()` must be unique. Registration fails if the ID already exists.
- `OriginManager` takes ownership of the pointer via `std::unique_ptr`.
- The origin appears in `OriginSelectorWidget` immediately (the widget listens to `signalOriginRegistered`).
- The built-in `"local"` origin cannot be replaced.
**Unregistration** (if your module is unloaded):
```cpp
OriginManager::instance()->unregisterOrigin("my-plm");
```
If the unregistered origin was the current origin, `OriginManager` switches back to `"local"`.
## Step 10: Build integration
Add your files to `src/Gui/CMakeLists.txt`:
```cmake
SET(Gui_SRCS
# ... existing files ...
MyOrigin.cpp
MyOrigin.h
)
```
Include dependencies in your source file:
```cpp
#include "MyOrigin.h"
#include "BitmapFactory.h" // Icons
#include "Application.h" // Gui::Application
#include "FileDialog.h" // File dialogs
#include "MainWindow.h" // getMainWindow()
#include <App/Application.h> // App::GetApplication()
#include <App/Document.h> // App::Document
#include <Base/Console.h> // Logging
```
No changes are needed to `CommandOrigin.cpp`, `OriginSelectorWidget.cpp`, or `Workbench.cpp` — they discover origins dynamically through `OriginManager`.
## Implementation checklist
| Phase | Task | Status |
|-------|------|--------|
| Identity | `id()`, `name()`, `nickname()`, `icon()`, `type()` | |
| Ownership | `ownsDocument()` with tracking property check | |
| Identity | `documentIdentity()`, `documentDisplayId()` | |
| Tracking | Set tracking property on new/first-save | |
| Operations | `newDocument()`, `openDocument()`, `openDocumentInteractive()` | |
| Operations | `saveDocument()`, `saveDocumentAs()`, `saveDocumentAsInteractive()` | |
| Characteristics | `tracksExternally()`, `requiresAuthentication()` | |
| Capabilities | `supportsRevisions()`, `supportsBOM()`, `supportsPartNumbers()` | |
| Extended ops | `commitDocument()`, `pullDocument()`, `pushDocument()` (if revisions) | |
| Extended ops | `showInfo()` (if part numbers), `showBOM()` (if BOM) | |
| Connection | `connectionState()`, `connect()`, `disconnect()` (if auth) | |
| Registration | `registerOrigin()` call in module init | |
| Build | Source files added to `CMakeLists.txt` | |
## See also
- [FileOrigin Interface](./cpp-file-origin.md) — complete API reference
- [LocalFileOrigin](./cpp-local-file-origin.md) — reference implementation (simplest origin)
- [FileOriginPython Bridge](./cpp-file-origin-python.md) — how Python origins connect to the C++ layer
- [Creating a Custom Origin in Python](./python-custom-origin-guide.md) — Python alternative (no rebuild needed)
- [OriginManager](./cpp-origin-manager.md) — registration and document resolution
- [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar UI integration
- [CommandOrigin](./cpp-command-origin.md) — command dispatch

View File

@@ -0,0 +1,257 @@
# FileOriginPython — Python-to-C++ Bridge
> **Header:** `src/Gui/FileOriginPython.h`
> **Implementation:** `src/Gui/FileOriginPython.cpp`
> **Python binding:** `src/Gui/ApplicationPy.cpp`
> **Namespace:** `Gui`
`FileOriginPython` is an Adapter that wraps a Python object and presents
it as a C++ [FileOrigin](./cpp-file-origin.md). This is how Python
addons (like SiloOrigin) integrate with the C++
[OriginManager](./cpp-origin-manager.md) without compiling any C++ code.
## Registration from Python
The bridge is exposed to Python through three `FreeCADGui` module
functions:
```python
import FreeCADGui
FreeCADGui.addOrigin(obj) # register a Python origin
FreeCADGui.removeOrigin(obj) # unregister it
FreeCADGui.getOrigin(obj) # look up the wrapper (or None)
```
### `FreeCADGui.addOrigin(obj)`
1. Checks that `obj` is not already registered (duplicate check via
`findOrigin`).
2. Constructs a `FileOriginPython` wrapper around `obj`.
3. Calls `obj.id()` immediately and caches the result — the ID must be
non-empty or registration fails.
4. Passes the wrapper to
`OriginManager::instance()->registerOrigin(wrapper)`.
5. If `registerOrigin` fails (e.g., duplicate ID), the wrapper is removed
from the internal instances list (OriginManager already deleted it).
### `FreeCADGui.removeOrigin(obj)`
1. Finds the wrapper via `findOrigin(obj)`.
2. Removes it from the internal `_instances` vector.
3. Calls `OriginManager::instance()->unregisterOrigin(id)`, which deletes
the wrapper.
### Typical Usage (from SiloOrigin)
```python
# mods/silo/freecad/silo_origin.py
class SiloOrigin:
def id(self): return "silo"
def name(self): return "Kindred Silo"
# ... remaining interface methods ...
_silo_origin = SiloOrigin()
def register_silo_origin():
FreeCADGui.addOrigin(_silo_origin)
def unregister_silo_origin():
FreeCADGui.removeOrigin(_silo_origin)
```
This is called from `InitGui.py` via a deferred `QTimer.singleShot`
(1500 ms after GUI init) to ensure the Gui module is fully loaded.
## Method Dispatch
Every C++ `FileOrigin` virtual method is implemented by delegating to the
corresponding Python method on the wrapped object. Three internal
helpers handle the marshalling:
| Helper | Signature | Used For |
|--------|-----------|----------|
| `callStringMethod` | `(name, default) → std::string` | `id()`, `name()`, `nickname()`, `icon()` |
| `callBoolMethod` | `(name, default) → bool` | All `supportsXxx()`, `tracksExternally()`, etc. |
| `callMethod` | `(name, args) → Py::Object` | Everything else (document ops, state queries) |
### Dispatch Pattern
Each overridden method follows the same structure:
```
1. Acquire GIL ← Base::PyGILStateLocker lock
2. Check if Python object has attr ← _inst.hasAttr("methodName")
3. If missing → return default ← graceful degradation
4. Build Py::Tuple args ← marshal C++ args to Python
5. Call Python method ← func.apply(args)
6. Convert result ← Py::String/Boolean/Long → C++ type
7. Return ← release GIL on scope exit
```
If the Python method is missing, the bridge returns sensible defaults
(empty string, `false`, `nullptr`). If the Python method raises an
exception, it is caught, reported to the FreeCAD console via
`Base::PyException::reportException()`, and the default is returned.
### Document Argument Marshalling
Methods that take `App::Document*` convert it to a Python object using:
```cpp
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
{
if (!doc) return Py::None();
return Py::asObject(doc->getPyObject());
}
```
This returns the standard `App.Document` Python wrapper so the Python
origin can call `doc.Objects`, `doc.FileName`, `doc.save()`, etc.
Methods that **return** an `App::Document*` (like `newDocument`,
`openDocument`) check the return value with:
```cpp
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
}
```
If the Python method returns `None` or a non-Document object, the bridge
returns `nullptr`.
## GIL Management
Every method acquires the GIL (`Base::PyGILStateLocker`) before touching
any Python objects. This is critical because the C++ origin methods can
be called from:
- The main Qt event loop (menu clicks, toolbar actions)
- `OriginManager::findOwningOrigin()` during document operations
- Signal handlers (fastsignals callbacks)
All of these may run on the main thread while the GIL is not held.
## Enum Mapping
Python origins return integers for enum values:
### `OriginType` (from `type()`)
| Python `int` | C++ Enum |
|-------------|----------|
| `0` | `OriginType::Local` |
| `1` | `OriginType::PLM` |
| `2` | `OriginType::Cloud` |
| `3` | `OriginType::Custom` |
### `ConnectionState` (from `connectionState()`)
| Python `int` | C++ Enum |
|-------------|----------|
| `0` | `ConnectionState::Disconnected` |
| `1` | `ConnectionState::Connecting` |
| `2` | `ConnectionState::Connected` |
| `3` | `ConnectionState::Error` |
Out-of-range values fall back to `OriginType::Custom` and
`ConnectionState::Connected` respectively.
## Icon Resolution
The `icon()` override calls `callStringMethod("icon")` to get a string
name, then passes it to `BitmapFactory().iconFromTheme(name)`. The
Python origin returns an icon name (e.g., `"silo"`) rather than a QIcon
object.
## Required Python Interface
Methods the Python object **must** implement (called unconditionally):
| Method | Signature | Purpose |
|--------|-----------|---------|
| `id()` | `→ str` | Unique origin ID (cached on registration) |
| `name()` | `→ str` | Display name |
| `nickname()` | `→ str` | Short toolbar label |
| `icon()` | `→ str` | Icon name for `BitmapFactory` |
| `type()` | `→ int` | `OriginType` enum value |
| `tracksExternally()` | `→ bool` | Whether origin syncs externally |
| `requiresAuthentication()` | `→ bool` | Whether login is needed |
| `ownsDocument(doc)` | `→ bool` | Ownership detection |
| `documentIdentity(doc)` | `→ str` | Immutable tracking key |
| `documentDisplayId(doc)` | `→ str` | Human-readable ID |
Methods the Python object **may** implement (checked with `hasAttr`):
| Method | Signature | Default |
|--------|-----------|---------|
| `supportsRevisions()` | `→ bool` | `False` |
| `supportsBOM()` | `→ bool` | `False` |
| `supportsPartNumbers()` | `→ bool` | `False` |
| `supportsAssemblies()` | `→ bool` | `False` |
| `connectionState()` | `→ int` | `2` (Connected) |
| `connect()` | `→ bool` | `True` |
| `disconnect()` | `→ None` | no-op |
| `syncProperties(doc)` | `→ bool` | `True` |
| `newDocument(name)` | `→ Document` | `None` |
| `openDocument(identity)` | `→ Document` | `None` |
| `openDocumentInteractive()` | `→ Document` | `None` |
| `saveDocument(doc)` | `→ bool` | `False` |
| `saveDocumentAs(doc, id)` | `→ bool` | `False` |
| `saveDocumentAsInteractive(doc)` | `→ bool` | `False` |
| `commitDocument(doc)` | `→ bool` | `False` |
| `pullDocument(doc)` | `→ bool` | `False` |
| `pushDocument(doc)` | `→ bool` | `False` |
| `showInfo(doc)` | `→ None` | no-op |
| `showBOM(doc)` | `→ None` | no-op |
## Error Handling
All Python exceptions are caught and reported to the FreeCAD console.
The bridge **never** propagates a Python exception into C++ — callers
always receive a safe default value.
```
[Python exception in SiloOrigin.ownsDocument]
→ Base::PyException::reportException()
→ FreeCAD Console: "Python error: ..."
→ return false
```
This prevents a buggy Python origin from crashing the application.
## Lifetime and Ownership
```
FreeCADGui.addOrigin(py_obj)
├─ FileOriginPython(py_obj) ← wraps, holds Py::Object ref
│ └─ _inst = py_obj ← prevents Python GC
├─ _instances.push_back(wrapper) ← static vector for findOrigin()
└─ OriginManager::registerOrigin(wrapper)
└─ unique_ptr<FileOrigin> ← OriginManager owns the wrapper
```
- `FileOriginPython` holds a `Py::Object` reference to the Python
instance, preventing garbage collection.
- `OriginManager` owns the wrapper via `std::unique_ptr`.
- The static `_instances` vector provides `findOrigin()` lookup but does
**not** own the pointers.
- On `removeOrigin()`: the wrapper is removed from `_instances`, then
`OriginManager::unregisterOrigin()` deletes the wrapper, which releases
the `Py::Object` reference.
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
being adapted
- [OriginManager](./cpp-origin-manager.md) — where the wrapped origin
gets registered
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch
through this bridge
- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md)
— step-by-step guide using this bridge

View File

@@ -0,0 +1,199 @@
# FileOrigin — Abstract Interface
> **Header:** `src/Gui/FileOrigin.h`
> **Implementation:** `src/Gui/FileOrigin.cpp`
> **Namespace:** `Gui`
`FileOrigin` is the abstract base class that all document storage backends
implement. It defines the contract for creating, opening, saving, and
tracking FreeCAD documents through a pluggable origin system.
## Key Design Principle
Origins do **not** change where files are stored — all documents are always
saved to the local filesystem. Origins change the **workflow** and
**identity model**:
| Origin | Identity | Tracking | Authentication |
|--------|----------|----------|----------------|
| Local | File path | None | None |
| PLM (Silo) | Database UUID | External DB + MinIO | Required |
| Cloud | URL / key | External service | Required |
## Enums
### `OriginType`
```cpp
enum class OriginType {
Local, // Local filesystem storage
PLM, // Product Lifecycle Management system (e.g., Silo)
Cloud, // Generic cloud storage
Custom // User-defined origin type
};
```
### `ConnectionState`
```cpp
enum class ConnectionState {
Disconnected, // Not connected
Connecting, // Connection in progress
Connected, // Successfully connected
Error // Connection error occurred
};
```
Used by the [OriginSelectorWidget](./cpp-origin-selector-widget.md) to
render status overlays on origin icons.
## Pure Virtual Methods (must override)
Every `FileOrigin` subclass must implement these methods.
### Identity
| Method | Return | Purpose |
|--------|--------|---------|
| `id()` | `std::string` | Unique origin ID (`"local"`, `"silo"`, …) |
| `name()` | `std::string` | Display name for menus (`"Local Files"`, `"Kindred Silo"`) |
| `nickname()` | `std::string` | Short label for toolbar (`"Local"`, `"Silo"`) |
| `icon()` | `QIcon` | Icon for UI representation |
| `type()` | `OriginType` | Classification enum |
### Workflow Characteristics
| Method | Return | Purpose |
|--------|--------|---------|
| `tracksExternally()` | `bool` | `true` if origin syncs to a remote system |
| `requiresAuthentication()` | `bool` | `true` if origin needs login |
### Document Identity
| Method | Return | Purpose |
|--------|--------|---------|
| `documentIdentity(doc)` | `std::string` | Immutable tracking key. File path for Local, UUID for PLM. |
| `documentDisplayId(doc)` | `std::string` | Human-readable ID. File path for Local, part number for PLM. |
| `ownsDocument(doc)` | `bool` | Whether this origin owns the document. |
**Ownership detection** is the mechanism that determines which origin
manages a given document. The
[OriginManager](./cpp-origin-manager.md) calls `ownsDocument()` on each
registered origin to resolve ownership.
- `LocalFileOrigin` owns documents that have **no** `SiloItemId` property
on any object.
- `SiloOrigin` (Python) owns documents where any object **has** a
`SiloItemId` property.
### Core Document Operations
| Method | Parameters | Return | Purpose |
|--------|-----------|--------|---------|
| `newDocument` | `name = ""` | `App::Document*` | Create a new document |
| `openDocument` | `identity` | `App::Document*` | Open by identity (non-interactive) |
| `openDocumentInteractive` | — | `App::Document*` | Open via dialog (file picker / search) |
| `saveDocument` | `doc` | `bool` | Save document |
| `saveDocumentAs` | `doc, newIdentity` | `bool` | Save with new identity |
| `saveDocumentAsInteractive` | `doc` | `bool` | Save via dialog |
Returns `nullptr` / `false` on failure or cancellation.
## Virtual Methods with Defaults (optional overrides)
### Capability Queries
These default to `false`. Override to advertise capabilities that the
[CommandOrigin](./cpp-command-origin.md) commands check before enabling
menu items.
| Method | Default | Enables |
|--------|---------|---------|
| `supportsRevisions()` | `false` | Commit / Pull / Push commands |
| `supportsBOM()` | `false` | BOM command |
| `supportsPartNumbers()` | `false` | Info command |
| `supportsAssemblies()` | `false` | (reserved for future use) |
### Connection State
| Method | Default | Purpose |
|--------|---------|---------|
| `connectionState()` | `Connected` | Current connection state |
| `connect()` | `return true` | Attempt to connect / authenticate |
| `disconnect()` | no-op | Disconnect from origin |
### Extended PLM Operations
These default to no-op / `false`. Override in PLM origins.
| Method | Default | Purpose |
|--------|---------|---------|
| `commitDocument(doc)` | `false` | Create a versioned snapshot |
| `pullDocument(doc)` | `false` | Fetch latest from remote |
| `pushDocument(doc)` | `false` | Upload changes to remote |
| `showInfo(doc)` | no-op | Show metadata dialog |
| `showBOM(doc)` | no-op | Show Bill of Materials dialog |
| `syncProperties(doc)` | `true` | Push property changes to backend |
## Signal
```cpp
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
```
Origins must emit this signal when their connection state changes. The
`OriginSelectorWidget` subscribes to it to update the toolbar icon
overlay (green = connected, red X = disconnected, warning = error).
## Construction and Lifetime
- `FileOrigin` is non-copyable (deleted copy constructor and assignment
operator).
- The protected default constructor prevents direct instantiation.
- Instances are owned by `OriginManager` via `std::unique_ptr`.
- Python origins are wrapped by `FileOriginPython` (see
[Python-C++ bridge](./cpp-file-origin-python.md)).
## LocalFileOrigin
`LocalFileOrigin` is the built-in concrete implementation that ships with
Kindred Create. It is always registered by `OriginManager` as the
`"local"` origin and serves as the universal fallback.
### Behavior Summary
| Method | Behavior |
|--------|----------|
| `ownsDocument` | Returns `true` if **no** object has a `SiloItemId` property |
| `documentIdentity` | Returns `doc->FileName` (full path) |
| `newDocument` | Delegates to `App::GetApplication().newDocument()` |
| `openDocument` | Delegates to `App::GetApplication().openDocument()` |
| `openDocumentInteractive` | Shows standard FreeCAD file-open dialog with format filters |
| `saveDocument` | Calls `doc->save()`, returns `false` if no filename set |
| `saveDocumentAs` | Calls `doc->saveAs()` |
| `saveDocumentAsInteractive` | Calls `Gui::Document::saveAs()` (shows save dialog) |
### Ownership Detection Algorithm
```
for each object in document:
if object has property "SiloItemId":
return false ← owned by PLM, not local
return true ← local owns this document
```
This negative-match approach means `LocalFileOrigin` is the **universal
fallback** — it owns any document that no other origin claims.
## See Also
- [OriginManager](./cpp-origin-manager.md) — singleton registry that
manages origin instances
- [FileOriginPython](./cpp-file-origin-python.md) — bridge for
implementing origins in Python
- [CommandOrigin](./cpp-command-origin.md) — File menu commands that
dispatch to origins
- [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar
dropdown for switching origins
- [Creating a Custom Origin (C++)](../guide/custom-origin-cpp.md)
- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md)

View File

@@ -0,0 +1,218 @@
# LocalFileOrigin
`LocalFileOrigin` is the built-in, default `FileOrigin` implementation. It handles documents stored on the local filesystem without external tracking or synchronisation.
- **Header:** `src/Gui/FileOrigin.h`
- **Source:** `src/Gui/FileOrigin.cpp`
- **Origin ID:** `"local"`
- **Display name:** `"Local Files"` (nickname `"Local"`)
- **Type:** `OriginType::Local`
## Design principles
1. **Identity is the file path.** A document's identity under LocalFileOrigin is its `FileName` property — the absolute path on disk.
2. **Ownership by exclusion.** LocalFileOrigin claims every document that is *not* claimed by a PLM origin. It detects this by scanning for the `SiloItemId` property; if no object in the document has it, the document is local.
3. **Always available.** LocalFileOrigin is created by `OriginManager` at startup and cannot be unregistered. It is the mandatory fallback origin.
4. **Thin delegation.** Most operations delegate directly to `App::GetApplication()` or `App::Document` methods. LocalFileOrigin adds no persistence layer of its own.
## Identity and capabilities
```cpp
std::string id() const override; // "local"
std::string name() const override; // "Local Files"
std::string nickname() const override; // "Local"
QIcon icon() const override; // theme icon "document-new"
OriginType type() const override; // OriginType::Local
```
### Capability flags
All capability queries return `false`:
| Method | Returns | Reason |
|--------|---------|--------|
| `tracksExternally()` | `false` | No external database or server |
| `requiresAuthentication()` | `false` | No login required |
| `supportsRevisions()` | `false` | No revision history |
| `supportsBOM()` | `false` | No Bill of Materials |
| `supportsPartNumbers()` | `false` | No part number system |
| `supportsAssemblies()` | `false` | No native assembly tracking |
These flags control which toolbar buttons and menu items are enabled. With all flags false, the origin toolbar commands (Commit, Pull, Push, Info, BOM) are disabled for local documents.
### Connection state
LocalFileOrigin uses the base-class defaults — it is always `ConnectionState::Connected`:
| Method | Behaviour |
|--------|-----------|
| `connectionState()` | Returns `ConnectionState::Connected` |
| `connect()` | Returns `true` immediately |
| `disconnect()` | No-op |
| `signalConnectionStateChanged` | Never emitted |
## Ownership detection
```cpp
bool ownsDocument(App::Document* doc) const override;
```
**Algorithm:**
1. Return `false` if `doc` is null.
2. Iterate every object in the document (`doc->getObjects()`).
3. For each object, check `obj->getPropertyByName("SiloItemId")`.
4. If **any** object has the property, return `false` — the document belongs to a PLM origin.
5. If **no** object has it, return `true`.
The constant `SiloItemId` is defined as a `static const char*` in `FileOrigin.cpp`.
**Edge cases:**
- An empty document (no objects) is owned by LocalFileOrigin.
- A document where only some objects have `SiloItemId` is still considered PLM-owned.
- Performance is O(n) where n is the number of objects in the document.
### Ownership resolution order
`OriginManager::findOwningOrigin()` checks non-local origins first, then falls back to LocalFileOrigin. This ensures PLM origins with specific ownership criteria take priority:
```
for each registered origin (excluding "local"):
if origin.ownsDocument(doc) → return that origin
if localOrigin.ownsDocument(doc) → return localOrigin
return nullptr
```
Results are cached in `OriginManager::_documentOrigins` for O(1) subsequent lookups.
## Document identity
```cpp
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
```
Both return the same value: the document's `FileName` property (its absolute filesystem path). Returns an empty string if the document is null, not owned, or has never been saved.
For PLM origins these would differ — `documentIdentity` might return a UUID while `documentDisplayId` returns a part number. For local files, the path serves both purposes.
## Document operations
### Creating documents
```cpp
App::Document* newDocument(const std::string& name = "") override;
```
Delegates to `App::GetApplication().newDocument()`. If `name` is empty, defaults to `"Unnamed"`. The returned document has no `FileName` set until saved.
### Opening documents
```cpp
App::Document* openDocument(const std::string& identity) override;
App::Document* openDocumentInteractive() override;
```
**`openDocument`** — Non-interactive. Takes a file path, delegates to `App::GetApplication().openDocument()`. Returns `nullptr` if the path is empty or the file cannot be loaded.
**`openDocumentInteractive`** — Shows a file dialog with the following behaviour:
1. Builds a format filter list from FreeCAD's registered import types, with `.FCStd` first.
2. Shows `FileDialog::getOpenFileNames()` (multi-file selection).
3. For each selected file, uses `SelectModule::importHandler()` to determine the loader module.
4. Calls `Application::Instance->open()` with the `UserInitiatedOpenDocument` flag set.
5. Runs `checkPartialRestore()` and `checkRestoreError()` for validation.
6. Returns the last loaded document, or `nullptr` if the dialog was cancelled.
### Saving documents
```cpp
bool saveDocument(App::Document* doc) override;
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
bool saveDocumentAsInteractive(App::Document* doc) override;
```
**`saveDocument`** — Requires that the document already has a `FileName` set. If the path is empty (never-saved document), returns `false`. Otherwise calls `doc->save()`.
**`saveDocumentAs`** — Non-interactive. Calls `doc->saveAs(newIdentity)` with the given path. Updates the document's `FileName` property as a side effect.
**`saveDocumentAsInteractive`** — Gets the `Gui::Document` wrapper and delegates to `guiDoc->saveAs()`, which shows a save dialog. Returns `false` if the GUI document cannot be found or the user cancels.
**Save workflow pattern:**
```
First save: saveDocument() → false (no path)
→ command layer calls saveDocumentAsInteractive()
→ user picks path → saveAs()
Later saves: saveDocument() → true (path exists)
→ doc->save()
```
### PLM operations (not implemented)
These inherited base-class methods are no-ops for LocalFileOrigin:
| Method | Returns | Behaviour |
|--------|---------|-----------|
| `commitDocument(doc)` | `false` | No external commits |
| `pullDocument(doc)` | `false` | No remote to pull from |
| `pushDocument(doc)` | `false` | No remote to push to |
| `showInfo(doc)` | void | No-op |
| `showBOM(doc)` | void | No-op |
## OriginManager integration
### Lifecycle
LocalFileOrigin is created in the `OriginManager` constructor via `ensureLocalOrigin()`:
```cpp
auto localOrigin = std::make_unique<LocalFileOrigin>();
_origins[LOCAL_ORIGIN_ID] = std::move(localOrigin);
_currentOriginId = LOCAL_ORIGIN_ID;
```
It is set as the default current origin. It is the only origin that exists before any Python workbenches load.
### Unregister protection
`OriginManager::unregisterOrigin()` explicitly blocks removal of the local origin:
```cpp
if (id == LOCAL_ORIGIN_ID) {
Base::Console().warning(
"OriginManager: Cannot unregister built-in local origin\n");
return false;
}
```
### Preference persistence
The current origin ID is saved to `User parameter:BaseApp/Preferences/General/Origin` under the key `CurrentOriginId`. On restart, if the saved ID is not `"local"` but the corresponding origin hasn't been registered yet, LocalFileOrigin remains the active origin until a Python workbench registers the saved origin.
## Constants
Defined in `FileOrigin.cpp`:
```cpp
static const char* SILO_ITEM_ID_PROP = "SiloItemId";
```
Defined in `OriginManager.cpp`:
```cpp
static const char* LOCAL_ORIGIN_ID = "local";
static const char* PREF_PATH = "User parameter:BaseApp/Preferences/General/Origin";
static const char* PREF_CURRENT_ORIGIN = "CurrentOriginId";
```
## See also
- [FileOrigin Interface](./cpp-file-origin.md) — abstract base class
- [OriginManager](./cpp-origin-manager.md) — singleton registry and document resolution
- [FileOriginPython Bridge](./cpp-file-origin-python.md) — Python adapter for custom origins
- [SiloOrigin](./python-silo-origin.md) — PLM origin that contrasts with LocalFileOrigin

View File

@@ -0,0 +1,226 @@
# OriginManager — Singleton Registry
> **Header:** `src/Gui/OriginManager.h`
> **Implementation:** `src/Gui/OriginManager.cpp`
> **Namespace:** `Gui`
`OriginManager` is the central singleton that tracks all registered
[FileOrigin](./cpp-file-origin.md) instances, maintains the user's current
origin selection, and resolves which origin owns a given document.
## Lifecycle
```
Application startup
├─ OriginManager::instance() ← singleton created
│ ├─ ensureLocalOrigin() ← "local" origin always exists
│ └─ loadPreferences() ← restore last-used origin from prefs
├─ Python addons register origins ← e.g. SiloOrigin via FreeCADGui.addOrigin()
├─ ... application runs ...
└─ OriginManager::destruct() ← saves prefs, deletes all origins
```
The `"local"` origin is created in the constructor and **cannot be
unregistered**. It serves as the universal fallback.
## Singleton Access
```cpp
// Get the instance (created on first call)
OriginManager* mgr = OriginManager::instance();
// Destroy at application shutdown
OriginManager::destruct();
```
## Origin Registration
### `registerOrigin(FileOrigin* origin) → bool`
Register a new origin. The manager takes **ownership** via
`std::unique_ptr` — do not delete the pointer after registration.
- Returns `false` and deletes the origin if:
- `origin` is `nullptr`
- `origin->id()` is empty
- An origin with the same ID is already registered
- Emits `signalOriginRegistered(id)` on success.
```cpp
auto* cloud = new MyCloudOrigin();
bool ok = OriginManager::instance()->registerOrigin(cloud);
// cloud is now owned by OriginManager — do not delete it
```
### `unregisterOrigin(const std::string& id) → bool`
Remove and delete an origin by ID.
- Returns `false` if:
- `id` is `"local"` (built-in, cannot be removed)
- No origin with that ID exists
- If the unregistered origin was the current origin, the current origin
automatically reverts to `"local"`.
- Emits `signalOriginUnregistered(id)` on success.
### `getOrigin(const std::string& id) → FileOrigin*`
Look up an origin by ID. Returns `nullptr` if not found.
### `originIds() → std::vector<std::string>`
Returns all registered origin IDs. Order is the `std::map` key order
(alphabetical).
## Current Origin Selection
The "current origin" determines which backend is used for **new
documents** (File > New) and appears selected in the
[OriginSelectorWidget](./cpp-origin-selector-widget.md) toolbar dropdown.
### `currentOrigin() → FileOrigin*`
Returns the currently selected origin. Never returns `nullptr` — falls
back to `"local"` if the stored ID is somehow invalid.
### `currentOriginId() → std::string`
Returns the current origin's ID string.
### `setCurrentOrigin(const std::string& id) → bool`
Switch the current origin. Returns `false` if the ID is not registered.
- Persists the selection to FreeCAD preferences at
`User parameter:BaseApp/Preferences/General/Origin/CurrentOriginId`.
- Emits `signalCurrentOriginChanged(id)` when the selection actually
changes (no signal if setting to the already-current origin).
### `originForNewDocument() → FileOrigin*`
Convenience method — returns `currentOrigin()`. Called by the File > New
command to determine which origin should handle new document creation.
## Document-Origin Resolution
When FreeCAD needs to know which origin owns a document (e.g., for
File > Save), it uses the resolution chain below.
### `originForDocument(App::Document* doc) → FileOrigin*`
Primary lookup method. Resolution order:
1. **Explicit association** — check the `_documentOrigins` cache for a
prior `setDocumentOrigin()` call.
2. **Ownership detection** — call `findOwningOrigin(doc)` to scan all
origins.
3. **Cache the result** — store in `_documentOrigins` for future lookups.
Returns `nullptr` only if no origin claims the document (should not
happen in practice since `LocalFileOrigin` is the universal fallback).
### `findOwningOrigin(App::Document* doc) → FileOrigin*`
Scans all registered origins by calling `ownsDocument(doc)` on each.
**Algorithm:**
```
1. For each origin where id ≠ "local":
if origin->ownsDocument(doc):
return origin ← PLM/Cloud origin claims it
2. if localOrigin->ownsDocument(doc):
return localOrigin ← fallback to local
3. return nullptr ← no owner (shouldn't happen)
```
Non-local origins are checked **first** because they have specific
ownership criteria (e.g., presence of `SiloItemId` property). The local
origin uses negative-match logic (owns anything not claimed by others),
so it must be checked last to avoid false positives.
### `setDocumentOrigin(doc, origin)`
Explicitly associate a document with an origin. Used when creating new
documents to mark them with the origin that created them.
- Passing `origin = nullptr` clears the association.
- Emits `signalDocumentOriginChanged(doc, originId)`.
### `clearDocumentOrigin(doc)`
Remove a document from the association cache. Called when a document is
closed to prevent stale pointers.
## Signals
All signals use the [fastsignals](https://github.com/nicktrandafil/fastsignals)
library (not Qt signals).
| Signal | Parameters | When |
|--------|-----------|------|
| `signalOriginRegistered` | `const std::string& id` | After a new origin is registered |
| `signalOriginUnregistered` | `const std::string& id` | After an origin is removed |
| `signalCurrentOriginChanged` | `const std::string& id` | After the user switches the current origin |
| `signalDocumentOriginChanged` | `App::Document*, const std::string& id` | After a document-origin association changes |
### Subscribers
- **[OriginSelectorWidget](./cpp-origin-selector-widget.md)** subscribes
to `signalOriginRegistered`, `signalOriginUnregistered`, and
`signalCurrentOriginChanged` to rebuild the dropdown menu and update
the toolbar button.
- **[CommandOrigin](./cpp-command-origin.md)** commands query the manager
on each `isActive()` call to check document ownership and origin
capabilities.
## Preference Persistence
| Path | Key | Type | Default |
|------|-----|------|---------|
| `User parameter:BaseApp/Preferences/General/Origin` | `CurrentOriginId` | ASCII string | `"local"` |
Loaded in the constructor, saved on destruction and on each
`setCurrentOrigin()` call. If the saved origin ID is not registered at
load time (e.g., the Silo addon hasn't loaded yet), the manager falls
back to `"local"` and will re-check when origins are registered later.
## Memory Model
```
OriginManager (singleton)
├─ _origins: std::map<string, std::unique_ptr<FileOrigin>>
│ ├─ "local" → LocalFileOrigin (always present)
│ ├─ "silo" → FileOriginPython (wraps Python SiloOrigin)
│ └─ ... → other registered origins
├─ _currentOriginId: std::string
└─ _documentOrigins: std::map<App::Document*, string>
├─ doc1 → "silo"
├─ doc2 → "local"
└─ ... (cache, cleared on document close)
```
- Origins are owned exclusively by the manager via `unique_ptr`.
- The document-origin map uses raw `App::Document*` pointers. Callers
**must** call `clearDocumentOrigin()` when a document is destroyed to
prevent dangling pointers.
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
that registered origins implement
- [FileOriginPython](./cpp-file-origin-python.md) — bridge for
Python-implemented origins
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch to
origins via this manager
- [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar UI
driven by this manager's signals

View File

@@ -0,0 +1,224 @@
# OriginSelectorWidget
`OriginSelectorWidget` is a toolbar dropdown that lets users switch between registered file origins (Local Files, Silo, etc.). It appears as the first item in the File toolbar across all workbenches.
- **Header:** `src/Gui/OriginSelectorWidget.h`
- **Source:** `src/Gui/OriginSelectorWidget.cpp`
- **Base class:** `QToolButton`
- **Command ID:** `Std_Origin`
## Widget appearance
The button shows the current origin's nickname and icon. Clicking opens a dropdown menu listing all registered origins with a checkmark on the active one.
```
┌────────────────┐
│ Local ▼ │ ← nickname + icon, InstantPopup mode
└────────────────┘
Dropdown:
✓ Local ← checked = current origin
Kindred Silo ← disconnected origins show status overlay
──────────────────
Manage Origins... ← opens OriginManagerDialog
```
### Size constraints
| Property | Value |
|----------|-------|
| Popup mode | `QToolButton::InstantPopup` |
| Button style | `Qt::ToolButtonTextBesideIcon` |
| Minimum width | 80 px |
| Maximum width | 160 px |
| Size policy | `Preferred, Fixed` |
## Lifecycle
### Construction
The constructor calls four setup methods in order:
1. **`setupUi()`** — Configures the button, creates `m_menu` (QMenu) and `m_originActions` (exclusive QActionGroup). Connects the action group's `triggered` signal to `onOriginActionTriggered`.
2. **`connectSignals()`** — Subscribes to three OriginManager fastsignals via `scoped_connection` objects.
3. **`rebuildMenu()`** — Populates the menu from the current OriginManager state.
4. **`updateDisplay()`** — Sets the button text, icon, and tooltip to match the current origin.
### Destruction
The destructor calls `disconnectSignals()`, which explicitly disconnects the three `fastsignals::scoped_connection` members. The scoped connections would auto-disconnect on destruction regardless, but explicit disconnection prevents any signal delivery during teardown.
## Signal connections
The widget subscribes to three `OriginManager` fastsignals:
| OriginManager signal | Widget handler | Response |
|----------------------|----------------|----------|
| `signalOriginRegistered` | `onOriginRegistered(id)` | Rebuild menu |
| `signalOriginUnregistered` | `onOriginUnregistered(id)` | Rebuild menu |
| `signalCurrentOriginChanged` | `onCurrentOriginChanged(id)` | Update display + menu checkmarks |
Connections are stored as `fastsignals::scoped_connection` members for RAII lifetime management:
```cpp
fastsignals::scoped_connection m_connRegistered;
fastsignals::scoped_connection m_connUnregistered;
fastsignals::scoped_connection m_connChanged;
```
## Menu population
`rebuildMenu()` rebuilds the entire dropdown from scratch each time an origin is registered or unregistered:
1. Clear `m_menu` and remove all actions from `m_originActions`.
2. Iterate `OriginManager::originIds()` (returns `std::vector<std::string>` in registration order).
3. For each origin ID, create a `QAction` with:
- Icon from `iconForOrigin(origin)` (with connection-state overlay)
- Text from `origin->nickname()`
- Tooltip from `origin->name()`
- `setCheckable(true)`, checked if this is the current origin
- Data set to the origin ID string
4. Add a separator.
5. Add a "Manage Origins..." action with the `preferences-system` theme icon, connected to `onManageOriginsClicked`.
Origins appear in the menu in the order returned by `OriginManager::originIds()`. The local origin is always first (registered at startup), followed by Python-registered origins in registration order.
## Origin selection
When the user clicks an origin in the dropdown, `onOriginActionTriggered(QAction*)` runs:
1. Extract the origin ID from `action->data()`.
2. Look up the `FileOrigin` from OriginManager.
3. **Authentication gate:** If `origin->requiresAuthentication()` is true and the connection state is `Disconnected` or `Error`:
- Call `origin->connect()`.
- If connection fails, revert the menu checkmark to the previous origin and return without changing.
4. On success, call `OriginManager::setCurrentOrigin(originId)`.
This means selecting a disconnected PLM origin triggers an automatic reconnection attempt. The user sees no change if the connection fails.
## Icon overlays
`iconForOrigin(FileOrigin*)` generates a display icon with optional connection-state indicators:
| Connection state | Overlay | Position |
|------------------|---------|----------|
| `Connected` | None | — |
| `Connecting` | None (TODO: animated) | — |
| `Disconnected` | `dagViewFail` (8x8 px, red) | Bottom-right |
| `Error` | `Warning` (8x8 px, yellow) | Bottom-right |
Overlays are only applied to origins where `requiresAuthentication()` returns `true`. Local origins never get overlays. The merge uses `BitmapFactoryInst::mergePixmap` with `BottomRight` placement.
## Display updates
`updateDisplay()` sets the button face to reflect the current origin:
- **Text:** `origin->nickname()` (e.g. "Local", "Silo")
- **Icon:** Result of `iconForOrigin(origin)` (with possible overlay)
- **Tooltip:** `origin->name()` (e.g. "Local Files", "Kindred Silo")
- **No origin:** Text becomes "No Origin", icon and tooltip are cleared
This method runs on construction, on `signalCurrentOriginChanged`, and after the Manage Origins dialog closes.
## Command wrapper
The widget is exposed to the command/toolbar system through two classes.
### StdCmdOrigin
Defined in `src/Gui/CommandStd.cpp` using `DEF_STD_CMD_AC(StdCmdOrigin)`:
```cpp
StdCmdOrigin::StdCmdOrigin()
: Command("Std_Origin")
{
sGroup = "File";
sMenuText = QT_TR_NOOP("&Origin");
sToolTipText = QT_TR_NOOP("Select file origin (Local Files, Silo, etc.)");
sWhatsThis = "Std_Origin";
sStatusTip = sToolTipText;
sPixmap = "folder";
eType = 0;
}
```
| Property | Value |
|----------|-------|
| Command ID | `Std_Origin` |
| Menu group | `File` |
| Icon | `folder` |
| `isActive()` | Always `true` |
| `activated()` | No-op (widget handles interaction) |
`createAction()` returns an `OriginSelectorAction` instance.
### OriginSelectorAction
Defined in `src/Gui/Action.h` / `Action.cpp`. Bridges the command system and the widget:
```cpp
void OriginSelectorAction::addTo(QWidget* widget)
{
if (widget->inherits("QToolBar")) {
auto* selector = new OriginSelectorWidget(widget);
static_cast<QToolBar*>(widget)->addWidget(selector);
} else {
widget->addAction(action());
}
}
```
When added to a `QToolBar`, it instantiates an `OriginSelectorWidget`. When added to a menu or other container, it falls back to adding a plain `QAction`.
## Toolbar integration
`StdWorkbench::setupToolBars()` in `src/Gui/Workbench.cpp` places the widget:
```cpp
auto file = new ToolBarItem(root);
file->setCommand("File");
*file << "Std_Origin" // ← origin selector (first)
<< "Std_New"
<< "Std_Open"
<< "Std_Save";
```
The widget appears as the **first item** in the File toolbar. Because `StdWorkbench` is the base workbench, this placement is inherited by all workbenches (Part Design, Assembly, Silo, etc.).
A separate "Origin Tools" toolbar follows with PLM-specific commands:
```cpp
auto originTools = new ToolBarItem(root);
originTools->setCommand("Origin Tools");
*originTools << "Origin_Commit" << "Origin_Pull" << "Origin_Push"
<< "Separator"
<< "Origin_Info" << "Origin_BOM";
```
These commands auto-disable based on the current origin's capability flags (`supportsRevisions`, `supportsBOM`, etc.).
## Member variables
```cpp
QMenu* m_menu; // dropdown menu
QActionGroup* m_originActions; // exclusive checkmark group
QAction* m_manageAction; // "Manage Origins..." action
fastsignals::scoped_connection m_connRegistered; // signalOriginRegistered
fastsignals::scoped_connection m_connUnregistered; // signalOriginUnregistered
fastsignals::scoped_connection m_connChanged; // signalCurrentOriginChanged
```
## Behavioural notes
- **Origin scope is global, not per-document.** Switching origin affects all subsequent New/Open/Save operations. Existing open documents retain their origin association via `OriginManager::_documentOrigins`.
- **Menu rebuilds are full rebuilds.** On any registration/unregistration event, the entire menu is cleared and rebuilt. This is simple and correct — origin counts are small (typically 2-3).
- **No document-switch tracking.** The widget does not respond to active document changes. The current origin is a user preference, not derived from the active document.
- **Thread safety.** All operations assume the Qt main thread. Signal emissions from OriginManager are synchronous.
## See also
- [FileOrigin Interface](./cpp-file-origin.md) — abstract base class
- [LocalFileOrigin](./cpp-local-file-origin.md) — built-in default origin
- [OriginManager](./cpp-origin-manager.md) — singleton registry and signal source
- [CommandOrigin](./cpp-command-origin.md) — PLM commands in the Origin Tools toolbar

View File

@@ -0,0 +1,53 @@
# Glossary
## Terms
**BOM** — Bill of Materials. A structured list of components in an assembly with part numbers, quantities, and reference designators.
**Catppuccin Mocha** — A dark color palette used for Kindred Create's theme. Part of the [Catppuccin](https://github.com/catppuccin/catppuccin) color scheme family.
**Datum** — Reference geometry (plane, axis, or point) used as a construction aid for modeling. Not a physical shape — used to position features relative to abstract references.
**FCStd** — FreeCAD's standard document format. A ZIP archive containing XML model trees, BREP geometry, thumbnails, and embedded data.
**FileOrigin** — Abstract C++ interface in `src/Gui/` that defines a pluggable file backend. Implementations: `LocalFileOrigin` (filesystem) and `SiloOrigin` (database).
**FreeCAD** — Open-source parametric 3D CAD platform. Kindred Create is based on FreeCAD v1.0.0. Website: <https://www.freecad.org>
**Kindred Create** — The full application: a FreeCAD fork plus Kindred's addon workbenches, theme, and tooling.
**Manipulator** — FreeCAD mechanism for injecting commands from one workbench into another's menus and toolbars. Used by ztools to add commands to PartDesign.
**MinIO** — S3-compatible object storage server. Used by Silo to store binary `.FCStd` files.
**OndselSolver** — Lagrangian constraint solver for the Assembly workbench. Vendored as a submodule from a Kindred fork.
**Origin** — In Kindred Create context: the pluggable file backend system. In FreeCAD context: the coordinate system origin (X/Y/Z axes and planes) of a Part or Body.
**pixi** — Conda-based dependency manager and task runner. Used for all build operations. Website: <https://pixi.sh>
**Preference pack** — FreeCAD mechanism for bundling theme settings, preferences, and stylesheets into an installable package.
**QSS** — Qt Style Sheet. A CSS-like language for styling Qt widgets. Kindred Create's theme is defined in `KindredCreate.qss`.
**rattler-build** — Cross-platform package build tool from the conda ecosystem. Used to create AppImage, DMG, and NSIS installer bundles.
**Silo** — Kindred's parts database system. Consists of a Go server, FreeCAD workbench, and shared Python client library.
**SSE** — Server-Sent Events. HTTP-based protocol for real-time server-to-client notifications. Used by Silo for the activity feed.
**Workbench** — FreeCAD's plugin/module system. Each workbench provides a set of tools, menus, and toolbars for a specific task domain.
**ztools** — Kindred's unified workbench combining Part Design, Assembly, and Sketcher tools with custom datum creation and assembly patterns.
## Repository URLs
| Repository | URL |
|------------|-----|
| Kindred Create | <https://git.kindred-systems.com/kindred/create> |
| ztools | <https://git.kindred-systems.com/forbes/ztools> |
| silo-mod | <https://git.kindred-systems.com/kindred/silo-mod> |
| OndselSolver | <https://git.kindred-systems.com/kindred/solver> |
| GSL | <https://github.com/microsoft/GSL> |
| AddonManager | <https://github.com/FreeCAD/AddonManager> |
| googletest | <https://github.com/google/googletest> |

View File

@@ -0,0 +1,226 @@
# Silo Authentication Architecture
## Overview
Silo supports three authentication backends that can be enabled independently or in combination:
| Backend | Use Case | Config Key |
|---------|----------|------------|
| **Local** | Username/password stored in Silo's database (bcrypt) | `auth.local` |
| **LDAP** | FreeIPA / Active Directory via LDAP bind | `auth.ldap` |
| **OIDC** | Keycloak or any OpenID Connect provider (redirect flow) | `auth.oidc` |
When authentication is disabled (`auth.enabled: false`), all routes are open and a synthetic developer user with the `admin` role is injected into every request.
## Authentication Flow
### Browser (Session-Based)
```
User -> /login (GET) -> Renders login form
User -> /login (POST) -> Validates credentials via backends
-> Creates server-side session (PostgreSQL)
-> Sets silo_session cookie
-> Redirects to / or ?next= URL
User -> /auth/oidc (GET) -> Generates state, stores in session
-> Redirects to Keycloak authorize endpoint
Keycloak -> /auth/callback -> Verifies state, exchanges code for token
-> Extracts claims, upserts user in DB
-> Creates session, redirects to /
```
### API Client (Token-Based)
```
Client -> Authorization: Bearer silo_<hex> -> SHA-256 hash lookup
-> Validates expiry + user active
-> Injects user into context
```
Both paths converge at the `RequireAuth` middleware, which injects an `auth.User` into the request context. All downstream handlers use `auth.UserFromContext(ctx)` to access the authenticated user.
## Database Schema
### users
The single identity table that all backends resolve to. LDAP and OIDC users are upserted on first login.
| Column | Type | Notes |
|--------|------|-------|
| `id` | UUID | Primary key |
| `username` | TEXT | Unique. FreeIPA `uid` or OIDC `preferred_username` |
| `display_name` | TEXT | Human-readable name |
| `email` | TEXT | Not unique-constrained |
| `password_hash` | TEXT | NULL for LDAP/OIDC-only users (bcrypt cost 12) |
| `auth_source` | TEXT | `local`, `ldap`, or `oidc` |
| `oidc_subject` | TEXT | Stable OIDC `sub` claim (unique partial index) |
| `role` | TEXT | `admin`, `editor`, or `viewer` |
| `is_active` | BOOLEAN | Deactivated users are rejected at middleware |
| `last_login_at` | TIMESTAMPTZ | Updated on each successful login |
| `created_at` | TIMESTAMPTZ | |
| `updated_at` | TIMESTAMPTZ | |
### api_tokens
| Column | Type | Notes |
|--------|------|-------|
| `id` | UUID | Primary key |
| `user_id` | UUID | FK to users, CASCADE on delete |
| `name` | TEXT | Human-readable label |
| `token_hash` | TEXT | SHA-256 of raw token (unique) |
| `token_prefix` | TEXT | `silo_` + 8 hex chars for display |
| `scopes` | TEXT[] | Reserved for future fine-grained permissions |
| `last_used_at` | TIMESTAMPTZ | Updated asynchronously on use |
| `expires_at` | TIMESTAMPTZ | NULL = never expires |
| `created_at` | TIMESTAMPTZ | |
Raw token format: `silo_` + 64 hex characters (32 random bytes). Shown once at creation. Only the SHA-256 hash is stored.
### sessions
Required by `alexedwards/scs` pgxstore:
| Column | Type | Notes |
|--------|------|-------|
| `token` | TEXT | Primary key (session ID) |
| `data` | BYTEA | Serialized session data |
| `expiry` | TIMESTAMPTZ | Indexed for cleanup |
Session data contains `user_id` and `username`. Cookie: `silo_session`, HttpOnly, SameSite=Lax, 24h lifetime. `Secure` flag is set when `auth.enabled` is true.
### audit_log
| Column | Type | Notes |
|--------|------|-------|
| `id` | UUID | Primary key |
| `timestamp` | TIMESTAMPTZ | Indexed DESC |
| `user_id` | UUID | FK to users, SET NULL on delete |
| `username` | TEXT | Preserved after user deletion |
| `action` | TEXT | `create`, `update`, `delete`, `login`, etc. |
| `resource_type` | TEXT | `item`, `revision`, `project`, `relationship` |
| `resource_id` | TEXT | |
| `details` | JSONB | Arbitrary structured data |
| `ip_address` | TEXT | |
### User Tracking Columns
Migration 009 adds `created_by` and `updated_by` TEXT columns to:
- `items` (created_by, updated_by)
- `relationships` (created_by, updated_by)
- `projects` (created_by)
- `sync_log` (triggered_by)
These store the `username` string (not a foreign key) so audit records survive user deletion and dev mode uses `"dev"`.
## Role Model
Three roles with a strict hierarchy:
```
admin > editor > viewer
```
| Permission | viewer | editor | admin |
|-----------|--------|--------|-------|
| Read items, projects, schemas, BOMs | Yes | Yes | Yes |
| Create/update items and revisions | No | Yes | Yes |
| Upload files | No | Yes | Yes |
| Manage BOMs | No | Yes | Yes |
| Create/update projects | No | Yes | Yes |
| Import CSV / BOM CSV | No | Yes | Yes |
| Generate part numbers | No | Yes | Yes |
| Manage own API tokens | Yes | Yes | Yes |
| User management (future) | No | No | Yes |
Role enforcement uses `auth.RoleSatisfies(userRole, minimumRole)` which checks the hierarchy. A user with `admin` satisfies any minimum role.
### Role Mapping from External Sources
**LDAP/FreeIPA**: Mapped from group membership. Checked in priority order (admin > editor > viewer). First match wins.
```yaml
ldap:
role_mapping:
admin:
- "cn=silo-admins,cn=groups,cn=accounts,dc=kindred,dc=internal"
editor:
- "cn=silo-users,cn=groups,cn=accounts,dc=kindred,dc=internal"
- "cn=engineers,cn=groups,cn=accounts,dc=kindred,dc=internal"
viewer:
- "cn=silo-viewers,cn=groups,cn=accounts,dc=kindred,dc=internal"
```
**OIDC/Keycloak**: Mapped from `realm_access.roles` in the ID token.
```yaml
oidc:
admin_role: "silo-admin"
editor_role: "silo-editor"
default_role: "viewer"
```
Roles are re-evaluated on every external login. If an LDAP user's group membership changes in FreeIPA, their Silo role updates on their next login.
## User Lifecycle
| Event | What Happens |
|-------|-------------|
| First LDAP login | User row created with `auth_source=ldap`, role from group mapping |
| First OIDC login | User row created with `auth_source=oidc`, `oidc_subject` set |
| Subsequent external login | `display_name`, `email`, `role` updated; `last_login_at` updated |
| Local account creation | Admin creates user with username, password, role |
| Deactivation | `is_active=false` — sessions and tokens rejected at middleware |
| Password change | Only for `auth_source=local` users |
## Default Admin Account
On startup, if `auth.local.default_admin_username` and `auth.local.default_admin_password` are both set, the daemon checks for an existing user with that username. If none exists, it creates a local admin account. This is idempotent — subsequent startups skip creation.
```yaml
auth:
local:
enabled: true
default_admin_username: "admin"
default_admin_password: "" # Use SILO_ADMIN_PASSWORD env var
```
Set via environment variables for production:
```sh
export SILO_ADMIN_USERNAME=admin
export SILO_ADMIN_PASSWORD=<strong-password>
```
## API Token Security
- Raw token: `silo_` + 64 hex characters (32 bytes of `crypto/rand`)
- Storage: SHA-256 hash only — the raw token cannot be recovered from the database
- Display prefix: `silo_` + first 8 hex characters (for identification in UI)
- Tokens inherit the owning user's role. If the user is deactivated, all their tokens stop working
- Revocation is immediate (row deletion)
- `last_used_at` is updated asynchronously to avoid slowing down API requests
## Dev Mode
When `auth.enabled: false`:
- No login is required
- A synthetic user is injected into every request:
- Username: `dev`
- Role: `admin`
- Auth source: `local`
- `created_by` fields are set to `"dev"`
- CORS allows all origins
- Session cookies are not marked `Secure`
## Security Considerations
- **Password hashing**: bcrypt with cost factor 12
- **Token entropy**: 256 bits (32 bytes from `crypto/rand`)
- **LDAP**: Always use `ldaps://` (TLS). `tls_skip_verify` is available but should only be used for testing
- **OIDC state parameter**: 128 bits, stored server-side in session, verified on callback
- **Session cookies**: HttpOnly, SameSite=Lax, Secure when auth enabled
- **CSRF protection**: nosurf library on all web form routes. API routes exempt (use Bearer tokens instead)
- **CORS**: Locked down to configured origins when auth is enabled. Credentials allowed for session cookies

View File

@@ -0,0 +1,185 @@
# Silo Auth Middleware
## Middleware Chain
Every request passes through this middleware stack in order:
```
RequestID Assigns X-Request-ID header
|
RealIP Extracts client IP from X-Forwarded-For
|
RequestLogger Logs method, path, status, duration (zerolog)
|
Recoverer Catches panics, logs stack trace, returns 500
|
CORS Validates origin, sets Access-Control headers
|
SessionLoadAndSave Loads session from PostgreSQL, saves on response
|
[route group middleware applied per-group below]
```
## Route Groups
### Public (no auth)
```
GET /health
GET /ready
GET /login
POST /login
POST /logout
GET /auth/oidc
GET /auth/callback
```
No authentication or CSRF middleware. The login form includes a CSRF hidden field but nosurf is not enforced on these routes (they are outside the CSRF-protected group).
### Web UI (auth + CSRF)
```
RequireAuth -> CSRFProtect -> Handler
GET / Items page
GET /projects Projects page
GET /schemas Schemas page
GET /settings Settings page (account info, tokens)
POST /settings/tokens Create API token (form)
POST /settings/tokens/{id}/revoke Revoke token (form)
```
Both `RequireAuth` and `CSRFProtect` are applied. Form submissions must include a `csrf_token` hidden field with the value from `nosurf.Token(r)`.
### API (auth, no CSRF)
```
RequireAuth -> [RequireRole where needed] -> Handler
GET /api/auth/me Current user info (viewer)
GET /api/auth/tokens List tokens (viewer)
POST /api/auth/tokens Create token (viewer)
DELETE /api/auth/tokens/{id} Revoke token (viewer)
GET /api/schemas/* Read schemas (viewer)
POST /api/schemas/*/segments/* Modify schema values (editor)
GET /api/projects/* Read projects (viewer)
POST /api/projects Create project (editor)
PUT /api/projects/{code} Update project (editor)
DELETE /api/projects/{code} Delete project (editor)
GET /api/items/* Read items (viewer)
POST /api/items Create item (editor)
PUT /api/items/{partNumber} Update item (editor)
DELETE /api/items/{partNumber} Delete item (editor)
POST /api/items/{partNumber}/file Upload file (editor)
POST /api/items/import CSV import (editor)
GET /api/items/{partNumber}/bom/* Read BOM (viewer)
POST /api/items/{partNumber}/bom Add BOM entry (editor)
PUT /api/items/{partNumber}/bom/* Update BOM entry (editor)
DELETE /api/items/{partNumber}/bom/* Delete BOM entry (editor)
POST /api/generate-part-number Generate PN (editor)
GET /api/integrations/odoo/* Read Odoo config (viewer)
PUT /api/integrations/odoo/* Modify Odoo config (editor)
POST /api/integrations/odoo/* Odoo sync operations (editor)
```
API routes are exempt from CSRF (they use Bearer token auth). CORS credentials are allowed so browser-based API clients with session cookies work.
## RequireAuth
`internal/api/middleware.go`
Authentication check order:
1. **Auth disabled?** Inject synthetic dev user (`admin` role) and continue
2. **Bearer token?** Extract from `Authorization: Bearer silo_...` header, validate via `auth.Service.ValidateToken()`. On success, inject user into context
3. **Session cookie?** Read `user_id` from `scs` session, look up user via `auth.Service.GetUserByID()`. On success, inject user into context. On stale session (user not found), destroy session
4. **None of the above?**
- API requests (`/api/*`): Return `401 Unauthorized` JSON
- Web requests: Redirect to `/login?next=<current-path>`
## RequireRole
`internal/api/middleware.go`
Applied as per-group middleware on routes that require a minimum role:
```go
r.Use(server.RequireRole(auth.RoleEditor))
```
Checks `auth.RoleSatisfies(user.Role, minimum)` against the hierarchy `admin > editor > viewer`. Returns:
- `401 Unauthorized` if no user in context (should not happen after RequireAuth)
- `403 Forbidden` with message `"Insufficient permissions: requires <role> role"` if role is too low
## CSRFProtect
`internal/api/middleware.go`
Wraps the `justinas/nosurf` library:
- Cookie: `csrf_token`, HttpOnly, SameSite=Lax, Secure when auth enabled
- Exempt paths: `/api/*`, `/health`, `/ready`
- Form field name: `csrf_token`
- Failure: Returns `403 Forbidden` with `"CSRF token validation failed"`
Templates inject the token via `{{.CSRFToken}}` which is populated from `nosurf.Token(r)`.
## CORS Configuration
Configured in `internal/api/routes.go`:
| Setting | Auth Disabled | Auth Enabled |
|---------|---------------|--------------|
| Allowed Origins | `*` | From `auth.cors.allowed_origins` config |
| Allow Credentials | `false` | `true` (needed for session cookies) |
| Allowed Methods | GET, POST, PUT, PATCH, DELETE, OPTIONS | Same |
| Allowed Headers | Accept, Authorization, Content-Type, X-CSRF-Token, X-Request-ID | Same |
| Max Age | 300 seconds | 300 seconds |
FreeCAD uses direct HTTP (not browser), so CORS does not affect it. Browser-based tools on other origins need their origin in the allowed list.
## Request Flow Examples
### Browser Login
```
GET /projects
-> RequestLogger
-> CORS (pass)
-> Session (loads empty session)
-> RequireAuth: no token, no session user_id
-> Redirect 303 /login?next=/projects
POST /login (username=alice, password=...)
-> RequestLogger
-> CORS (pass)
-> Session (loads)
-> HandleLogin: auth.Authenticate(ctx, "alice", password)
-> Session: put user_id, renew token
-> Redirect 303 /projects
GET /projects
-> Session (loads, has user_id)
-> RequireAuth: session -> GetUserByID -> inject alice
-> CSRFProtect: GET request, passes
-> HandleProjectsPage: renders with alice's info
```
### API Token Request
```
GET /api/items
Authorization: Bearer silo_a1b2c3d4...
-> RequireAuth: Bearer token found
-> ValidateToken: SHA-256 hash lookup, check expiry, check user active
-> Inject user into context
-> HandleListItems: returns JSON
```

View File

@@ -0,0 +1,257 @@
# Silo Authentication User Guide
## Logging In
### Username and Password
Navigate to the Silo web UI. If authentication is enabled, you'll be redirected to the login page.
Enter your username and password. This works for both local accounts and LDAP/FreeIPA accounts — Silo tries local authentication first, then LDAP if configured.
### Keycloak / OIDC
If your deployment has OIDC enabled, the login page will show a "Sign in with Keycloak" button. Click it to be redirected to your identity provider. After authenticating there, you'll be redirected back to Silo with a session.
## Roles
Your role determines what you can do in Silo:
| Role | Permissions |
|------|-------------|
| **viewer** | Read all items, projects, schemas, BOMs. Manage own API tokens. |
| **editor** | Everything a viewer can do, plus: create/update/delete items, upload files, manage BOMs, import CSV, generate part numbers. |
| **admin** | Everything an editor can do, plus: user management (future), configuration changes. |
Your role is shown as a badge next to your name in the header. For LDAP and OIDC users, the role is determined by group membership or token claims and re-evaluated on each login.
## API Tokens
API tokens allow the FreeCAD plugin, scripts, and CI pipelines to authenticate with Silo without a browser session. Tokens inherit your role.
### Creating a Token (Web UI)
1. Click **Settings** in the navigation bar
2. Under **API Tokens**, enter a name (e.g., "FreeCAD workstation") and click **Create Token**
3. The raw token is displayed once — copy it immediately
4. Store the token securely. It cannot be shown again.
### Creating a Token (CLI)
```sh
export SILO_API_URL=https://silo.example.internal
export SILO_API_TOKEN=silo_<your-existing-token>
silo token create --name "CI pipeline"
```
Output:
```
Token created: CI pipeline
API Token: silo_a1b2c3d4e5f6...
Save this token — it will not be shown again.
```
### Listing Tokens
```sh
silo token list
```
### Revoking a Token
Via the web UI settings page (click **Revoke** next to the token), or via CLI:
```sh
silo token revoke <token-id>
```
Revocation is immediate. Any in-flight requests using the token will fail.
## FreeCAD Plugin Configuration
The FreeCAD plugin reads the API token from two sources (checked in order):
1. **FreeCAD Preferences**: `Tools > Edit parameters > BaseApp/Preferences/Mod/Silo > ApiToken`
2. **Environment variable**: `SILO_API_TOKEN`
To set the token in FreeCAD preferences:
1. Open FreeCAD
2. Go to `Edit > Preferences > General > Macro` or use the parameter editor
3. Navigate to `BaseApp/Preferences/Mod/Silo`
4. Set `ApiToken` to your token string (e.g., `silo_a1b2c3d4...`)
Or set the environment variable before launching FreeCAD:
```sh
export SILO_API_TOKEN=silo_a1b2c3d4...
freecad
```
The API URL is configured the same way via the `ApiUrl` preference or `SILO_API_URL` environment variable.
## Default Admin Account
On first deployment, configure a default admin account to bootstrap access:
```yaml
# config.yaml
auth:
enabled: true
local:
enabled: true
default_admin_username: "admin"
default_admin_password: "" # Set via SILO_ADMIN_PASSWORD env var
```
```sh
export SILO_ADMIN_PASSWORD=<strong-password>
```
The admin account is created on the first startup if it doesn't already exist. Subsequent startups skip creation. Change the password after first login via the database or a future admin UI.
## Configuration Reference
### Minimal (Local Auth Only)
```yaml
auth:
enabled: true
session_secret: "" # Set via SILO_SESSION_SECRET
local:
enabled: true
default_admin_username: "admin"
default_admin_password: "" # Set via SILO_ADMIN_PASSWORD
```
### LDAP / FreeIPA
```yaml
auth:
enabled: true
session_secret: ""
local:
enabled: true
default_admin_username: "admin"
default_admin_password: ""
ldap:
enabled: true
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"
email_attr: "mail"
display_attr: "displayName"
group_attr: "memberOf"
role_mapping:
admin:
- "cn=silo-admins,cn=groups,cn=accounts,dc=kindred,dc=internal"
editor:
- "cn=silo-users,cn=groups,cn=accounts,dc=kindred,dc=internal"
- "cn=engineers,cn=groups,cn=accounts,dc=kindred,dc=internal"
viewer:
- "cn=silo-viewers,cn=groups,cn=accounts,dc=kindred,dc=internal"
tls_skip_verify: false
```
### OIDC / Keycloak
```yaml
auth:
enabled: true
session_secret: ""
local:
enabled: true
oidc:
enabled: true
issuer_url: "https://keycloak.example.internal/realms/silo"
client_id: "silo"
client_secret: "" # Set via SILO_OIDC_CLIENT_SECRET
redirect_url: "https://silo.example.internal/auth/callback"
scopes: ["openid", "profile", "email"]
admin_role: "silo-admin"
editor_role: "silo-editor"
default_role: "viewer"
```
### CORS (Production)
```yaml
auth:
cors:
allowed_origins:
- "https://silo.example.internal"
```
## Environment Variables
| Variable | Description | Config Path |
|----------|-------------|-------------|
| `SILO_SESSION_SECRET` | Session encryption key | `auth.session_secret` |
| `SILO_ADMIN_USERNAME` | Default admin username | `auth.local.default_admin_username` |
| `SILO_ADMIN_PASSWORD` | Default admin password | `auth.local.default_admin_password` |
| `SILO_OIDC_CLIENT_SECRET` | OIDC client secret | `auth.oidc.client_secret` |
| `SILO_LDAP_BIND_PASSWORD` | LDAP service account password | `auth.ldap.bind_password` |
| `SILO_API_URL` | API base URL (CLI and FreeCAD) | — |
| `SILO_API_TOKEN` | API token (CLI and FreeCAD) | — |
Environment variables override config file values.
## Troubleshooting
### "Authentication required" on every request
- Verify `auth.enabled: true` in your config
- Check that the `sessions` table exists in PostgreSQL (migration 009)
- Ensure `SILO_SESSION_SECRET` is set (empty string is allowed for dev but not recommended)
- Check browser cookies — `silo_session` should be present after login
### API token returns 401
- Tokens are case-sensitive. Ensure no trailing whitespace
- Check token expiry with `silo token list`
- Verify the user account is still active
- Ensure the `Authorization` header format is exactly `Bearer silo_<hex>`
### LDAP login fails
- Check `ldaps://` URL is reachable from the Silo server
- Verify `base_dn` and `user_search_dn` match your FreeIPA tree
- Test with `ldapsearch` from the command line first
- Set `tls_skip_verify: true` temporarily to rule out certificate issues
- Check Silo logs for the specific LDAP error message
### OIDC redirect loops
- Verify `redirect_url` matches the Keycloak client configuration exactly
- Check that `issuer_url` is reachable from the Silo server
- Ensure the Keycloak client has the correct redirect URI registered
- Check for clock skew between Silo and Keycloak servers (JWT validation is time-sensitive)
### Locked out (no admin account)
If you've lost access to all admin accounts:
1. Set `auth.local.default_admin_password` to a new password via `SILO_ADMIN_PASSWORD`
2. Use a different username (e.g., `default_admin_username: "recovery-admin"`)
3. Restart Silo — the new account will be created
4. Log in and fix the original accounts
Or directly update the database:
```sql
-- Reset a local user's password (generate bcrypt hash externally)
UPDATE users SET password_hash = '<bcrypt-hash>', is_active = true WHERE username = 'admin';
```
### FreeCAD plugin gets 401
- 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.example.internal/api/items`

View File

View File

@@ -0,0 +1,16 @@
# LibreOffice Calc Extension
The Silo Calc extension has been moved to its own repository: [silo-calc](https://git.kindred-systems.com/kindred/silo-calc).
## Server-Side ODS Support
The server-side ODS library (`internal/ods/`) and ODS endpoints remain in this repository. See `docs/SPECIFICATION.md` Section 11 for the full endpoint listing.
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/items/export.ods` | Items as ODS |
| GET | `/api/items/template.ods` | Blank import template |
| POST | `/api/items/import.ods` | Import from ODS |
| GET | `/api/items/{pn}/bom/export.ods` | BOM as formatted ODS |
| GET | `/api/projects/{code}/sheet.ods` | Multi-sheet project workbook |
| POST | `/api/sheets/diff` | Upload ODS, return JSON diff |

View File

@@ -0,0 +1,523 @@
# Component Audit Tool
**Last Updated:** 2026-02-01
**Status:** Design
---
## Problem
The parts database has grown organically. Many items were created with only a
part number, description, and category. The property schema defines dozens of
fields per category (material, finish, manufacturer, supplier, cost, etc.) but
most items have few or none of these populated. There is no way to see which
items are missing data or to prioritize what needs filling in.
Currently, adding or updating properties requires either:
- Editing each item individually through the web UI detail panel
- Bulk CSV export, manual editing, re-import
- The Calc extension (new, not yet widely used)
None of these approaches give visibility into what's missing across the
database. Engineers don't know which items need attention until they encounter
a blank field during a design review or procurement cycle.
---
## Goals
1. Show a per-item completeness score based on the property schema
2. Surface the least-complete items so they can be prioritized
3. Let users fill in missing fields directly from the audit view
4. Filter by project, category, completeness threshold
5. Track improvement over time
---
## Design
The audit tool is a page in the web UI (`/audit`), built with the React SPA
(same architecture as the items, projects, and schemas pages). It adds one
new API endpoint for the completeness data and reuses existing endpoints for
updates.
### Completeness Scoring
Each item's completeness is computed against its category's property schema.
The schema defines both **global defaults** (12 fields, all categories) and
**category-specific properties** (varies: 9 fields for fasteners, 20+ for
motion components, etc.).
**Score formula:**
```
score = sum(weight for each filled field) / sum(weight for all applicable fields)
```
Score is 0.0 to 1.0, displayed as a percentage. Fields are weighted
differently depending on sourcing type.
**Purchased parts (`sourcing_type = "purchased"`):**
| Weight | Fields | Rationale |
|--------|--------|-----------|
| 3 | manufacturer_pn, sourcing_link | Can't procure without these |
| 2 | manufacturer, supplier, supplier_pn, standard_cost | Core procurement data |
| 1 | description, sourcing_type, lead_time_days, minimum_order_qty, lifecycle_status | Important but less blocking |
| 1 | All category-specific properties | Engineering detail |
| 0.5 | rohs_compliant, country_of_origin, notes, long_description | Nice to have |
**Manufactured parts (`sourcing_type = "manufactured"`):**
| Weight | Fields | Rationale |
|--------|--------|-----------|
| 3 | has_bom (at least one BOM child) | Can't manufacture without a BOM |
| 2 | description, standard_cost | Core identification |
| 1 | All category-specific properties | Engineering detail |
| 0.5 | manufacturer, supplier, notes, long_description | Less relevant for in-house |
The `has_bom` check for manufactured parts queries the `relationships`
table for at least one `rel_type = 'component'` child. This is not a
property field -- it's a structural check. A manufactured part with no BOM
children is flagged as critically incomplete regardless of how many other
fields are filled.
**Assemblies (categories A01-A07):**
Assembly scores are partially computed from children:
| Field | Source | Notes |
|-------|--------|-------|
| weight | Sum of child weights | Computed if all children have weight |
| standard_cost | Sum of child (cost * qty) | Computed from BOM |
| component_count | Count of BOM children | Always computable |
| has_bom | BOM children exist | Required (weight 3) |
A computed field counts as "filled" if the data needed to compute it is
available. If a computed value exists, it is shown alongside the stored
value so engineers can verify or override.
Assembly-specific properties that cannot be computed (assembly_time,
test_procedure, ip_rating, dimensions) are scored normally.
**Field filled criteria:**
- String fields: non-empty after trimming
- Number fields: non-null and non-zero
- Boolean fields: non-null (false is a valid answer)
- has_bom: at least one component relationship exists
Item-level fields (`description`, `sourcing_type`, `sourcing_link`,
`standard_cost`, `long_description`) are checked on the items table.
Property fields (`manufacturer`, `material`, etc.) are checked on the
current revision's `properties` JSONB column. BOM existence is checked
on the `relationships` table.
### Tiers
Items are grouped into completeness tiers for dashboard display:
| Tier | Range | Color | Label |
|------|-------|-------|-------|
| Critical | 0-25% | Red | Missing critical data |
| Low | 25-50% | Orange | Needs attention |
| Partial | 50-75% | Yellow | Partially complete |
| Good | 75-99% | Light green | Nearly complete |
| Complete | 100% | Green | All fields populated |
---
## API
### `GET /api/audit/completeness`
Returns completeness scores for all items (or filtered subset).
**Query parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `project` | string | Filter by project code |
| `category` | string | Filter by category prefix (e.g. `F`, `F01`) |
| `max_score` | float | Only items below this score (e.g. `0.5`) |
| `min_score` | float | Only items above this score |
| `sort` | string | `score_asc` (default), `score_desc`, `part_number`, `updated_at` |
| `limit` | int | Pagination limit (default 100) |
| `offset` | int | Pagination offset |
**Response:**
```json
{
"items": [
{
"part_number": "F01-0042",
"description": "M3x10 Socket Head Cap Screw",
"category": "F01",
"category_name": "Screws and Bolts",
"sourcing_type": "purchased",
"projects": ["3DX10", "PROTO"],
"score": 0.41,
"weighted_filled": 12.5,
"weighted_total": 30.5,
"has_bom": false,
"bom_children": 0,
"missing_critical": ["manufacturer_pn", "sourcing_link"],
"missing": [
"manufacturer_pn",
"sourcing_link",
"supplier",
"supplier_pn",
"finish",
"strength_grade",
"torque_spec"
],
"updated_at": "2026-01-15T10:30:00Z"
},
{
"part_number": "A01-0003",
"description": "3DX10 Line Assembly",
"category": "A01",
"category_name": "Mechanical Assembly",
"sourcing_type": "manufactured",
"projects": ["3DX10"],
"score": 0.68,
"weighted_filled": 15.0,
"weighted_total": 22.0,
"has_bom": true,
"bom_children": 12,
"computed_fields": {
"standard_cost": 7538.61,
"component_count": 12,
"weight": null
},
"missing_critical": [],
"missing": ["assembly_time", "test_procedure", "weight", "ip_rating"],
"updated_at": "2026-01-28T14:20:00Z"
}
],
"summary": {
"total_items": 847,
"avg_score": 0.42,
"manufactured_without_bom": 31,
"by_tier": {
"critical": 123,
"low": 298,
"partial": 251,
"good": 142,
"complete": 33
},
"by_category": {
"F": {"count": 156, "avg_score": 0.51},
"C": {"count": 89, "avg_score": 0.38},
"R": {"count": 201, "avg_score": 0.29}
}
}
}
```
### `GET /api/audit/completeness/{partNumber}`
Single-item detail with field-by-field breakdown.
```json
{
"part_number": "F01-0042",
"description": "M3x10 Socket Head Cap Screw",
"category": "F01",
"sourcing_type": "purchased",
"score": 0.41,
"has_bom": false,
"fields": [
{"key": "description", "source": "item", "weight": 1, "value": "M3x10 Socket Head Cap Screw", "filled": true},
{"key": "sourcing_type", "source": "item", "weight": 1, "value": "purchased", "filled": true},
{"key": "standard_cost", "source": "item", "weight": 2, "value": 0.12, "filled": true},
{"key": "sourcing_link", "source": "item", "weight": 3, "value": "", "filled": false},
{"key": "manufacturer", "source": "property", "weight": 2, "value": null, "filled": false},
{"key": "manufacturer_pn", "source": "property", "weight": 3, "value": null, "filled": false},
{"key": "supplier", "source": "property", "weight": 2, "value": null, "filled": false},
{"key": "supplier_pn", "source": "property", "weight": 2, "value": null, "filled": false},
{"key": "material", "source": "property", "weight": 1, "value": "18-8 Stainless Steel", "filled": true},
{"key": "finish", "source": "property", "weight": 1, "value": null, "filled": false},
{"key": "thread_size", "source": "property", "weight": 1, "value": "M3", "filled": true},
{"key": "thread_pitch", "source": "property", "weight": 1, "value": null, "filled": false},
{"key": "length", "source": "property", "weight": 1, "value": "10mm", "filled": true},
{"key": "head_type", "source": "property", "weight": 1, "value": "Socket", "filled": true}
]
}
```
For assemblies, the detail response includes a `computed_fields` section
showing values derived from children (cost rollup, weight rollup,
component count). These are presented alongside stored values in the UI
so engineers can compare and choose to accept the computed value.
Existing `PUT /api/items/{pn}` and revision property updates handle writes.
---
## Web UI
### Audit Page (`/audit`)
New page accessible from the top navigation bar (fourth tab after Items,
Projects, Schemas).
**Layout:**
```
+------------------------------------------------------------------+
| Items | Projects | Schemas | Audit |
+------------------------------------------------------------------+
| [Project: ___] [Category: ___] [Max Score: ___] [Search] |
+------------------------------------------------------------------+
| Summary Bar |
| [===Critical: 123===|===Low: 298===|==Partial: 251==|Good|Done] |
+------------------------------------------------------------------+
| Score | PN | Description | Category | Missing|
|-------|-----------|--------------------------|----------|--------|
| 12% | R01-0003 | Bearing, Deep Groove 6205| Bearings | 18 |
| 15% | E14-0001 | NTC Thermistor 10K | Sensors | 16 |
| 23% | C03-0012 | 1/4" NPT Ball Valve SS | Valves | 14 |
| 35% | F01-0042 | M3x10 Socket Head Cap | Screws | 7 |
| ... | | | | |
+------------------------------------------------------------------+
```
**Interactions:**
- Click a row to open an inline edit panel (right side, same split-panel
pattern as the items page)
- The edit panel shows all applicable fields for the category, with empty
fields highlighted
- Editing a field and pressing Enter/Tab saves immediately via API
- Score updates live after each save
- Summary bar updates as items are completed
- Click a tier segment in the summary bar to filter to that tier
### Inline Edit Panel
```
+----------------------------------+
| F01-0042 Score: 35% |
| M3x10 Socket Head Cap Screw |
+----------------------------------+
| -- Required -- |
| Description [M3x10 Socket H..] |
| Sourcing [purchased v ] |
+----------------------------------+
| -- Procurement -- |
| Manufacturer [________________] |
| Mfr PN [________________] |
| Supplier [________________] |
| Supplier PN [________________] |
| Cost [$0.12 ] |
| Sourcing Link[________________] |
| Lead Time [____ days ] |
+----------------------------------+
| -- Fastener Properties -- |
| Material [18-8 Stainless ] |
| Finish [________________] |
| Thread Size [M3 ] |
| Thread Pitch [________________] |
| Length [10mm ] |
| Head Type [Socket ] |
| Drive Type [________________] |
| Strength [________________] |
| Torque Spec [________________] |
+----------------------------------+
| [Save All] |
+----------------------------------+
```
Fields are grouped into sections: Required, Procurement (global defaults),
and category-specific properties. Empty fields have a subtle red left border.
Filled fields have a green left border. The score bar at the top updates as
fields are filled in.
---
## Implementation Plan
### Phase 1: API endpoint and scoring engine
New file: `internal/api/audit_handlers.go`
- `HandleAuditCompleteness` -- query items, join current revision properties,
compute scores against schema, return paginated JSON
- `HandleAuditItemDetail` -- single item with field-by-field breakdown
- Scoring logic in a helper function that takes item fields + revision
properties + category schema and returns score + missing list
Register routes:
- `GET /api/audit/completeness` (viewer role)
- `GET /api/audit/completeness/{partNumber}` (viewer role)
### Phase 2: Web UI page
New template: `internal/api/templates/audit.html`
- Same base template, Catppuccin Mocha theme, nav bar with Audit tab
- Summary bar with tier counts (colored segments)
- Sortable, filterable table
- Split-panel detail view on row click
- Vanilla JS fetch calls to audit and item update endpoints
Update `internal/api/web.go`:
- Add `HandleAuditPage` handler
- Register `GET /audit` route
Update `internal/api/templates/base.html`:
- Add Audit tab to navigation
### Phase 3: Inline editing
- Field save on blur/Enter via `PUT /api/items/{pn}` for item fields
- Property updates via `POST /api/items/{pn}/revisions` with updated
properties map
- Live score recalculation after save (re-fetch from audit detail endpoint)
- Batch "Save All" button for multiple field changes
### Phase 4: Tracking and reporting
- Store periodic score snapshots (daily cron or on-demand) in a new
`audit_snapshots` table for trend tracking
- Dashboard chart showing completeness improvement over time
- Per-project completeness summary on the projects page
- CSV export of audit results for offline review
### Phase 5: Batch AI assistance
Server-side OpenRouter integration for bulk property inference from existing
sourcing data. This extends the Calc extension's AI client pattern to the
backend.
**Workflow:**
1. Audit page shows items with sourcing links but missing properties
2. Engineer selects items (or filters to a category/project) and clicks
"AI Fill Properties"
3. Server fetches each item's sourcing link page content (or uses the
seller description from the item's metadata)
4. OpenRouter API call per item: system prompt describes the category's
property schema, user prompt provides the scraped/stored description
5. AI returns structured JSON with suggested property values
6. Results shown in a review table: item, field, current value, suggested
value, confidence indicator
7. Engineer checks/unchecks suggestions, clicks "Apply Selected"
8. Server writes accepted values as property updates (new revision)
**AI prompt structure:**
```
System: You are a parts data specialist. Given a product description
and a list of property fields with types, extract values for as many
fields as possible. Return JSON only.
User:
Category: F01 (Screws and Bolts)
Product: {seller_description or scraped page text}
Fields to extract:
- material (string): Material specification
- finish (string): Surface finish
- thread_size (string): Thread size designation
- thread_pitch (string): Thread pitch
- length (string): Fastener length with unit
- head_type (string): Head style
- drive_type (string): Drive type
- strength_grade (string): Strength/property class
```
**Rate limiting:** Queue items and process in batches of 10 with 1s delay
between batches to stay within OpenRouter rate limits. Show progress bar
in the UI.
**Cost control:** Use `openai/gpt-4.1-nano` by default (cheapest). Show
estimated cost before starting batch. Allow model override in settings.
---
## Database Changes
### Phase 1: None
Completeness is computed at query time from existing `items` +
`revisions.properties` data joined against the in-memory schema definition.
No new tables needed for the core feature.
### Phase 4: New table
```sql
CREATE TABLE IF NOT EXISTS audit_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
captured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
total_items INTEGER NOT NULL,
avg_score DECIMAL(5,4) NOT NULL,
by_tier JSONB NOT NULL,
by_category JSONB NOT NULL,
by_project JSONB NOT NULL
);
```
### Phase 5: None
AI suggestions are ephemeral (computed per request, not stored). Accepted
suggestions are written through the existing revision/property update path.
---
## Scoring Examples
### Purchased Fastener (F01)
**Weighted total: ~30.5 points**
| Field | Weight | Filled? | Points |
|-------|--------|---------|--------|
| manufacturer_pn | 3 | no | 0/3 |
| sourcing_link | 3 | no | 0/3 |
| manufacturer | 2 | no | 0/2 |
| supplier | 2 | no | 0/2 |
| supplier_pn | 2 | no | 0/2 |
| standard_cost | 2 | yes | 2/2 |
| description | 1 | yes | 1/1 |
| sourcing_type | 1 | yes | 1/1 |
| material | 1 | yes | 1/1 |
| thread_size | 1 | yes | 1/1 |
| length | 1 | yes | 1/1 |
| head_type | 1 | yes | 1/1 |
| drive_type | 1 | no | 0/1 |
| finish | 1 | no | 0/1 |
| ... (remaining) | 0.5-1 | no | 0/... |
**Score: 8/30.5 = 26%** -- "Low" tier, flagged because weight-3 fields
(manufacturer_pn, sourcing_link) are missing.
### Manufactured Assembly (A01)
**Weighted total: ~22 points**
| Field | Weight | Source | Points |
|-------|--------|--------|--------|
| has_bom | 3 | BOM query | 3/3 (12 children) |
| description | 2 | item | 2/2 |
| standard_cost | 2 | computed from children | 2/2 |
| component_count | 1 | computed (= 12) | 1/1 |
| weight | 1 | computed (needs children) | 0/1 (not all children have weight) |
| assembly_time | 1 | property | 0/1 |
| test_procedure | 1 | property | 0/1 |
| dimensions | 1 | property | 0/1 |
| ip_rating | 1 | property | 0/1 |
| ... (globals) | 0.5-1 | property | .../... |
**Score: ~15/22 = 68%** -- "Partial" tier, mostly complete because BOM
and cost are covered through children.
### Motor (R01) -- highest field count
30+ applicable fields across global defaults + motion-specific properties
(load, speed, power, voltage, current, torque, encoder, gear ratio...).
A motor with only description + cost + sourcing_type scores under 10%
because of the large denominator. Motors are the category most likely to
benefit from batch AI extraction from datasheets.

View File

@@ -0,0 +1,364 @@
# Configuration Reference
**Last Updated:** 2026-02-06
---
## Overview
Silo is configured via a YAML file. Copy the example and edit for your environment:
```bash
cp config.example.yaml config.yaml
```
The server reads the config file at startup:
```bash
./silod -config config.yaml # default: config.yaml
go run ./cmd/silod -config config.yaml
```
YAML values support environment variable expansion using `${VAR_NAME}` syntax. Environment variable overrides (listed per-key below) take precedence over YAML values.
---
## Server
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `server.host` | string | `"0.0.0.0"` | Bind address |
| `server.port` | int | `8080` | HTTP port |
| `server.base_url` | string | — | External URL (e.g. `https://silo.example.com`). Used for OIDC callback URLs and session cookie domain. Required when OIDC is enabled. |
| `server.read_only` | bool | `false` | Start in read-only mode. All write endpoints return 503. Can be toggled at runtime with `SIGUSR1`. |
```yaml
server:
host: "0.0.0.0"
port: 8080
base_url: "https://silo.example.com"
read_only: false
```
---
## Database
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `database.host` | string | — | `SILO_DB_HOST` | PostgreSQL host |
| `database.port` | int | `5432` | — | PostgreSQL port |
| `database.name` | string | — | `SILO_DB_NAME` | Database name |
| `database.user` | string | — | `SILO_DB_USER` | Database user |
| `database.password` | string | — | `SILO_DB_PASSWORD` | Database password |
| `database.sslmode` | string | `"require"` | — | SSL mode: `disable`, `require`, `verify-ca`, `verify-full` |
| `database.max_connections` | int | `10` | — | Connection pool size |
**SSL mode guidance:**
- `disable` — development only, no encryption
- `require` — encrypted but no certificate verification (default)
- `verify-ca` — verify server certificate is signed by trusted CA
- `verify-full` — verify CA and hostname match (recommended for production)
```yaml
database:
host: "localhost"
port: 5432
name: "silo"
user: "silo"
password: "" # use SILO_DB_PASSWORD env var
sslmode: "require"
max_connections: 10
```
---
## Storage (MinIO/S3)
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `storage.endpoint` | string | — | `SILO_MINIO_ENDPOINT` | MinIO/S3 endpoint (`host:port`) |
| `storage.access_key` | string | — | `SILO_MINIO_ACCESS_KEY` | Access key |
| `storage.secret_key` | string | — | `SILO_MINIO_SECRET_KEY` | Secret key |
| `storage.bucket` | string | — | — | S3 bucket name (created automatically if missing) |
| `storage.use_ssl` | bool | `false` | — | Use HTTPS for MinIO connections |
| `storage.region` | string | `"us-east-1"` | — | S3 region |
```yaml
storage:
endpoint: "localhost:9000"
access_key: "" # use SILO_MINIO_ACCESS_KEY env var
secret_key: "" # use SILO_MINIO_SECRET_KEY env var
bucket: "silo-files"
use_ssl: false
region: "us-east-1"
```
---
## Schemas
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `schemas.directory` | string | `"/etc/silo/schemas"` | Path to directory containing YAML schema files |
| `schemas.default` | string | — | Default schema name for part number generation |
Schema files define part numbering formats, category codes, and property definitions. See `schemas/kindred-rd.yaml` for an example.
```yaml
schemas:
directory: "./schemas"
default: "kindred-rd"
```
---
## FreeCAD
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `freecad.uri_scheme` | string | `"silo"` | URI scheme for "Open in FreeCAD" links in the web UI |
| `freecad.executable` | string | — | Path to FreeCAD binary (for CLI operations) |
```yaml
freecad:
uri_scheme: "silo"
executable: "/usr/bin/freecad"
```
---
## Odoo ERP Integration
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `odoo.enabled` | bool | `false` | Enable Odoo integration |
| `odoo.url` | string | — | Odoo server URL |
| `odoo.database` | string | — | Odoo database name |
| `odoo.username` | string | — | Odoo username |
| `odoo.api_key` | string | — | Odoo API key |
The Odoo integration currently supports configuration and sync-log CRUD. Push/pull sync operations are stubs.
```yaml
odoo:
enabled: false
url: "https://odoo.example.com"
database: "odoo"
username: "silo-service"
api_key: ""
```
---
## Authentication
Authentication has a master toggle and three independent backends. When `auth.enabled` is `false`, all routes are accessible without login and a synthetic admin user (`dev`) is injected into every request.
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `auth.enabled` | bool | `false` | — | Master toggle. Set `true` for production. |
| `auth.session_secret` | string | — | `SILO_SESSION_SECRET` | Secret for signing session cookies. Required when auth is enabled. |
### Local Auth
Built-in username/password accounts stored in the Silo database with bcrypt-hashed passwords.
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `auth.local.enabled` | bool | — | — | Enable local accounts |
| `auth.local.default_admin_username` | string | — | `SILO_ADMIN_USERNAME` | Default admin account created on first startup |
| `auth.local.default_admin_password` | string | — | `SILO_ADMIN_PASSWORD` | Password for default admin (bcrypt-hashed on creation) |
The default admin account is only created if both username and password are set and the user does not already exist. This is idempotent.
### LDAP / FreeIPA
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `auth.ldap.enabled` | bool | `false` | — | Enable LDAP authentication |
| `auth.ldap.url` | string | — | — | LDAP server URL (e.g. `ldaps://ipa.example.com`) |
| `auth.ldap.base_dn` | string | — | — | Base DN for the LDAP tree |
| `auth.ldap.user_search_dn` | string | — | — | DN under which to search for users |
| `auth.ldap.bind_dn` | string | — | — | Service account DN for user lookups (optional; omit for direct user bind) |
| `auth.ldap.bind_password` | string | — | `SILO_LDAP_BIND_PASSWORD` | Service account password |
| `auth.ldap.user_attr` | string | `"uid"` | — | LDAP attribute for username |
| `auth.ldap.email_attr` | string | `"mail"` | — | LDAP attribute for email |
| `auth.ldap.display_attr` | string | `"displayName"` | — | LDAP attribute for display name |
| `auth.ldap.group_attr` | string | `"memberOf"` | — | LDAP attribute for group membership |
| `auth.ldap.role_mapping` | map | — | — | Maps LDAP group DNs to Silo roles (see example below) |
| `auth.ldap.tls_skip_verify` | bool | `false` | — | Skip TLS certificate verification (testing only) |
**Role mapping** maps LDAP group DNs to Silo roles. Groups are checked in priority order: admin, then editor, then viewer. The first match wins.
```yaml
auth:
ldap:
enabled: true
url: "ldaps://ipa.example.com"
base_dn: "dc=example,dc=com"
user_search_dn: "cn=users,cn=accounts,dc=example,dc=com"
role_mapping:
admin:
- "cn=silo-admins,cn=groups,cn=accounts,dc=example,dc=com"
editor:
- "cn=silo-users,cn=groups,cn=accounts,dc=example,dc=com"
- "cn=engineers,cn=groups,cn=accounts,dc=example,dc=com"
viewer:
- "cn=silo-viewers,cn=groups,cn=accounts,dc=example,dc=com"
```
### OIDC / Keycloak
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `auth.oidc.enabled` | bool | `false` | — | Enable OIDC authentication |
| `auth.oidc.issuer_url` | string | — | — | OIDC provider issuer URL (e.g. Keycloak realm URL) |
| `auth.oidc.client_id` | string | — | — | OAuth2 client ID |
| `auth.oidc.client_secret` | string | — | `SILO_OIDC_CLIENT_SECRET` | OAuth2 client secret |
| `auth.oidc.redirect_url` | string | — | — | OAuth2 callback URL (typically `{base_url}/auth/callback`) |
| `auth.oidc.scopes` | []string | `["openid", "profile", "email"]` | — | OAuth2 scopes to request |
| `auth.oidc.admin_role` | string | — | — | Keycloak realm role that grants admin access |
| `auth.oidc.editor_role` | string | — | — | Keycloak realm role that grants editor access |
| `auth.oidc.default_role` | string | `"viewer"` | — | Fallback role when no role claim matches |
Roles are extracted from the Keycloak `realm_access.roles` claim. If the user has the `admin_role`, they get admin. Otherwise if they have `editor_role`, they get editor. Otherwise `default_role` applies.
```yaml
auth:
oidc:
enabled: true
issuer_url: "https://keycloak.example.com/realms/silo"
client_id: "silo"
client_secret: "" # use SILO_OIDC_CLIENT_SECRET env var
redirect_url: "https://silo.example.com/auth/callback"
admin_role: "silo-admin"
editor_role: "silo-editor"
default_role: "viewer"
```
### CORS
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `auth.cors.allowed_origins` | []string | — | Origins allowed for cross-origin requests from browser-based clients |
FreeCAD and other non-browser clients use direct HTTP and are not affected by CORS. This setting is for browser-based tools running on different origins.
```yaml
auth:
cors:
allowed_origins:
- "https://silo.example.com"
```
---
## Environment Variables
All environment variable overrides. These take precedence over values in `config.yaml`.
| Variable | Config Key | Description |
|----------|-----------|-------------|
| `SILO_DB_HOST` | `database.host` | PostgreSQL host |
| `SILO_DB_NAME` | `database.name` | PostgreSQL database name |
| `SILO_DB_USER` | `database.user` | PostgreSQL user |
| `SILO_DB_PASSWORD` | `database.password` | PostgreSQL password |
| `SILO_MINIO_ENDPOINT` | `storage.endpoint` | MinIO endpoint |
| `SILO_MINIO_ACCESS_KEY` | `storage.access_key` | MinIO access key |
| `SILO_MINIO_SECRET_KEY` | `storage.secret_key` | MinIO secret key |
| `SILO_SESSION_SECRET` | `auth.session_secret` | Session cookie signing secret |
| `SILO_ADMIN_USERNAME` | `auth.local.default_admin_username` | Default admin username |
| `SILO_ADMIN_PASSWORD` | `auth.local.default_admin_password` | Default admin password |
| `SILO_LDAP_BIND_PASSWORD` | `auth.ldap.bind_password` | LDAP service account password |
| `SILO_OIDC_CLIENT_SECRET` | `auth.oidc.client_secret` | OIDC client secret |
Additionally, YAML values can reference environment variables directly using `${VAR_NAME}` syntax, which is expanded at load time via `os.ExpandEnv()`.
---
## Deployment Examples
### Development (no auth)
```yaml
server:
host: "0.0.0.0"
port: 8080
base_url: "http://localhost:8080"
database:
host: "localhost"
port: 5432
name: "silo"
user: "silo"
password: "silodev"
sslmode: "disable"
storage:
endpoint: "localhost:9000"
access_key: "minioadmin"
secret_key: "minioadmin"
bucket: "silo-files"
use_ssl: false
schemas:
directory: "./schemas"
default: "kindred-rd"
auth:
enabled: false
```
### Local Auth Only
```yaml
auth:
enabled: true
session_secret: "change-me-to-a-random-string"
local:
enabled: true
default_admin_username: "admin"
default_admin_password: "change-me"
```
### LDAP / FreeIPA
```yaml
auth:
enabled: true
session_secret: "${SILO_SESSION_SECRET}"
local:
enabled: false
ldap:
enabled: true
url: "ldaps://ipa.example.com"
base_dn: "dc=example,dc=com"
user_search_dn: "cn=users,cn=accounts,dc=example,dc=com"
role_mapping:
admin:
- "cn=silo-admins,cn=groups,cn=accounts,dc=example,dc=com"
editor:
- "cn=engineers,cn=groups,cn=accounts,dc=example,dc=com"
viewer:
- "cn=silo-viewers,cn=groups,cn=accounts,dc=example,dc=com"
```
### OIDC / Keycloak
```yaml
auth:
enabled: true
session_secret: "${SILO_SESSION_SECRET}"
local:
enabled: false
oidc:
enabled: true
issuer_url: "https://keycloak.example.com/realms/silo"
client_id: "silo"
client_secret: "${SILO_OIDC_CLIENT_SECRET}"
redirect_url: "https://silo.example.com/auth/callback"
admin_role: "silo-admin"
editor_role: "silo-editor"
default_role: "viewer"
```

View File

@@ -0,0 +1,470 @@
# 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
- [Architecture](#architecture)
- [External Services](#external-services)
- [Quick Start](#quick-start)
- [Initial Setup](#initial-setup)
- [Deployment](#deployment)
- [Configuration](#configuration)
- [Maintenance](#maintenance)
- [Troubleshooting](#troubleshooting)
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ silo.example.internal │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ silod │ │
│ │ (Silo API Server) │ │
│ │ :8080 │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ psql.example.internal │ │ minio.example.internal │
│ PostgreSQL 16 │ │ MinIO S3 │
│ :5432 │ │ :9000 (API) │
│ │ │ :9001 (Console) │
└─────────────────────────┘ └─────────────────────────────────┘
```
## External Services
The following external services are already configured:
| Service | Host | Database/Bucket | User |
|---------|------|-----------------|------|
| PostgreSQL | psql.example.internal:5432 | silo | silo |
| MinIO | minio.example.internal:9000 | silo-files | silouser |
Migrations have been applied to the database.
---
## Quick Start
For a fresh VM, run these commands:
```bash
# 1. SSH to the target host
ssh root@silo.example.internal
# 2. Download and run setup script
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
# 4. Deploy
/opt/silo/src/scripts/deploy.sh
```
---
## Initial Setup
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.example.internal:/tmp/
ssh root@silo.example.internal 'bash /tmp/setup-host.sh'
# Option 2: Direct on the host
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
```
The setup script:
- Installs dependencies (git, Go 1.23)
- Creates the `silo` system user
- Creates directory structure (`/opt/silo`, `/etc/silo`)
- Clones the repository to `/opt/silo/src`
- Creates the environment file template
### Configure Credentials
After setup, edit the environment file with your credentials:
```bash
sudo nano /etc/silo/silod.env
```
Fill in the values:
```bash
# Database credentials (psql.example.internal)
SILO_DB_PASSWORD=your-database-password
# MinIO credentials (minio.example.internal)
SILO_MINIO_ACCESS_KEY=silouser
SILO_MINIO_SECRET_KEY=your-minio-secret-key
```
### Verify External Services
Before deploying, verify connectivity to external services:
```bash
# Test PostgreSQL
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
# Test MinIO
curl -I http://minio.example.internal:9000/minio/health/live
```
---
## Deployment
### Deploy (or Update)
To deploy or update Silo, run the deploy script on the target host:
```bash
ssh root@silo.example.internal
/opt/silo/src/scripts/deploy.sh
```
The deploy script:
1. Pulls the latest code from git
2. Builds the `silod` binary
3. Installs configuration and schemas
4. Installs/updates the systemd service
5. Restarts the service
6. Verifies health endpoints
### Deploy Options
```bash
# Full deployment (pull, build, deploy, restart)
sudo /opt/silo/src/scripts/deploy.sh
# 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
```
### Environment Variables
You can override the git repository URL and branch:
```bash
export SILO_REPO_URL=https://git.kindred-systems.com/kindred/silo.git
export SILO_BRANCH=main
sudo -E /opt/silo/src/scripts/deploy.sh
```
---
## Configuration
### File Locations
| File | Purpose |
|------|---------|
| `/opt/silo/bin/silod` | Server binary |
| `/opt/silo/src/` | Git repository checkout |
| `/etc/silo/config.yaml` | Server configuration |
| `/etc/silo/silod.env` | Environment variables (secrets) |
| `/etc/silo/schemas/` | Part numbering schemas |
| `/var/log/silo/` | Log directory |
### Configuration File
The configuration file `/etc/silo/config.yaml` is installed on first deployment and not overwritten on updates. To update it manually:
```bash
sudo cp /opt/silo/src/deployments/config.prod.yaml /etc/silo/config.yaml
sudo systemctl restart silod
```
### Schemas
Schemas in `/etc/silo/schemas/` are updated on every deployment from the repository.
---
## Maintenance
### Service Management
```bash
# Check status
sudo systemctl status silod
# Start/stop/restart
sudo systemctl start silod
sudo systemctl stop silod
sudo systemctl restart silod
# Enable/disable auto-start
sudo systemctl enable silod
sudo systemctl disable silod
```
### View Logs
```bash
# Follow logs
sudo journalctl -u silod -f
# Recent logs
sudo journalctl -u silod -n 100
# Logs since a time
sudo journalctl -u silod --since "1 hour ago"
sudo journalctl -u silod --since "2024-01-15 10:00:00"
```
### Health Checks
```bash
# Basic health check
curl http://localhost:8080/health
# Full readiness check (includes DB and MinIO)
curl http://localhost:8080/ready
```
### Update Deployment
To update to the latest version:
```bash
ssh root@silo.example.internal
/opt/silo/src/scripts/deploy.sh
```
To deploy a specific branch or tag:
```bash
cd /opt/silo/src
git fetch --all --tags
git checkout v1.2.3 # or a branch name
sudo /opt/silo/src/scripts/deploy.sh --no-pull
```
### Database Migrations
When new migrations are added, run them manually:
```bash
# Check for new migrations
ls -la /opt/silo/src/migrations/
# Run a specific migration
psql -h psql.example.internal -U silo -d silo -f /opt/silo/src/migrations/008_new_feature.sql
```
---
## Troubleshooting
### Service Won't Start
1. Check logs for errors:
```bash
sudo journalctl -u silod -n 50
```
2. Verify configuration:
```bash
cat /etc/silo/config.yaml
```
3. Check environment file permissions:
```bash
ls -la /etc/silo/silod.env
# Should be: -rw------- root silo
```
4. Verify binary exists:
```bash
ls -la /opt/silo/bin/silod
```
### Connection Refused to PostgreSQL
1. Test network connectivity:
```bash
nc -zv psql.example.internal 5432
```
2. Test credentials:
```bash
source /etc/silo/silod.env
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.
### Connection Refused to MinIO
1. Test network connectivity:
```bash
nc -zv minio.example.internal 9000
```
2. Test with curl:
```bash
curl -I http://minio.example.internal:9000/minio/health/live
```
3. Check SSL settings in config match MinIO setup:
```yaml
storage:
use_ssl: true # or false
```
### Health Check Fails
```bash
# Check individual endpoints
curl -v http://localhost:8080/health
curl -v http://localhost:8080/ready
# If ready fails but health passes, check external services
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
curl http://minio.example.internal:9000/minio/health/live
```
### Build Fails
1. Check Go is installed:
```bash
go version
# Should be 1.23+
```
2. Check source is present:
```bash
ls -la /opt/silo/src/
```
3. Try manual build:
```bash
cd /opt/silo/src
go build -v ./cmd/silod
```
---
## SSL/TLS with FreeIPA and Nginx
For production deployments, Silo should be served over HTTPS using nginx as a reverse proxy with certificates from FreeIPA.
### Setup IPA and Nginx
Run the IPA/nginx setup script after the basic host setup:
```bash
sudo /opt/silo/src/scripts/setup-ipa-nginx.sh
```
This script:
1. Installs FreeIPA client and nginx
2. Enrolls the host in FreeIPA domain
3. Requests SSL certificate from IPA CA (auto-renewed by certmonger)
4. Configures nginx as reverse proxy (HTTP → HTTPS redirect)
5. Opens firewall ports 80 and 443
### Manual Steps After Script
1. Verify certificate was issued:
```bash
getcert list
```
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.example.internal/health
```
### Certificate Management
Certificates are automatically renewed by certmonger. Check status:
```bash
# List all tracked certificates
getcert list
# Check specific certificate
getcert list -f /etc/ssl/silo/silo.crt
# Manual renewal if needed
getcert resubmit -f /etc/ssl/silo/silo.crt
```
### Trusting IPA CA on Clients
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.example.internal/ipa/config/ca.crt
# Ubuntu/Debian
sudo cp /tmp/ipa-ca.crt /usr/local/share/ca-certificates/ipa-ca.crt
sudo update-ca-certificates
# RHEL/Fedora
sudo cp /tmp/ipa-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
```
### Nginx Configuration
The nginx config is installed at `/etc/nginx/sites-available/silo`. Key settings:
- HTTP redirects to HTTPS
- TLS 1.2/1.3 only with strong ciphers
- Proxies to `127.0.0.1:8080` (silod)
- 100MB max upload size for CAD files
- Security headers (X-Frame-Options, etc.)
To modify:
```bash
sudo nano /etc/nginx/sites-available/silo
sudo nginx -t
sudo systemctl reload nginx
```
---
## Security Checklist
- [ ] `/etc/silo/silod.env` has mode 600 (`chmod 600`)
- [ ] Database password is strong and unique
- [ ] MinIO credentials are specific to silo (not admin)
- [ ] SSL/TLS enabled for PostgreSQL (`sslmode: require`)
- [ ] SSL/TLS enabled for MinIO (`use_ssl: true`) if available
- [ ] HTTPS enabled via nginx reverse proxy
- [ ] Silod listens on localhost only (`host: 127.0.0.1`)
- [ ] Firewall allows only ports 80, 443 (not 8080)
- [ ] Service runs as non-root `silo` user
- [ ] Host enrolled in FreeIPA for centralized auth (future)

View File

@@ -0,0 +1,452 @@
# Silo Gap Analysis and Revision Control Roadmap
**Date:** 2026-02-08
**Status:** Analysis Complete (Updated)
---
## Executive Summary
This document analyzes the current state of the Silo project against its specification, identifies documentation and feature gaps, and outlines a roadmap for enhanced revision control capabilities.
---
## 1. Documentation Gap Analysis
### 1.1 Current Documentation
| Document | Coverage | Status |
|----------|----------|--------|
| `README.md` | Quick start, overview, component map | Current |
| `docs/SPECIFICATION.md` | Design specification, API reference | Current |
| `docs/STATUS.md` | Implementation status summary | Current |
| `docs/DEPLOYMENT.md` | Production deployment guide | Current |
| `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)
#### High Priority
| Gap | Impact | Effort | Status |
|-----|--------|--------|--------|
| **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 | Complete (`docs/CONFIGURATION.md`) |
#### Medium Priority
| Gap | Impact | Effort |
|-----|--------|--------|
| **User Workflows** | Users lack step-by-step guidance | Medium |
| **Troubleshooting Guide** | Support burden increases | Medium |
| **Developer Setup Guide** | Onboarding friction | Low |
#### Lower Priority
| Gap | Impact | Effort |
|-----|--------|--------|
| **CHANGELOG.md** | Version history unclear | Low |
| **Architecture Decision Records** | Design rationale lost | Medium |
| **Integration Guide** | Third-party integration unclear | High |
### 1.3 Recommended Actions
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~~ Done (`docs/CONFIGURATION.md`)
5. **Create database schema guide**: Document migrations and troubleshooting
---
## 2. Current Revision Control Implementation
### 2.1 What's Implemented
| Feature | Status | Location |
|---------|--------|----------|
| Append-only revision history | Complete | `internal/db/items.go` |
| Sequential revision numbering | Complete | Database trigger |
| Property snapshots (JSONB) | Complete | `revisions.properties` |
| File versioning (MinIO) | Complete | `internal/storage/` |
| SHA256 checksums | Complete | Captured on upload |
| Revision comments | Complete | `revisions.comment` |
| User attribution | Complete | `revisions.created_by` |
| Property schema versioning | Complete | Migration 005 |
| BOM revision pinning | Complete | `relationships.child_revision` |
### 2.2 Database Schema
```sql
-- Current revision schema (migrations/001_initial.sql)
CREATE TABLE revisions (
id UUID PRIMARY KEY,
item_id UUID REFERENCES items(id) ON DELETE CASCADE,
revision_number INTEGER NOT NULL,
properties JSONB NOT NULL DEFAULT '{}',
file_key TEXT,
file_version TEXT, -- MinIO version ID
file_checksum TEXT, -- SHA256
file_size BIGINT,
thumbnail_key TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
created_by TEXT,
comment TEXT,
property_schema_version INTEGER DEFAULT 1,
UNIQUE(item_id, revision_number)
);
```
### 2.3 API Endpoints
| Endpoint | Method | Status |
|----------|--------|--------|
| `/api/items/{pn}/revisions` | GET | Implemented |
| `/api/items/{pn}/revisions` | POST | Implemented |
| `/api/items/{pn}/revisions/{rev}` | GET | Implemented |
| `/api/items/{pn}/file` | POST | Implemented |
| `/api/items/{pn}/file` | GET | Implemented (latest) |
| `/api/items/{pn}/file/{rev}` | GET | Implemented |
### 2.4 Client Integration
FreeCAD workbench maintained in separate [silo-mod](https://git.kindred-systems.com/kindred/silo-mod) repository. The server provides the revision and file endpoints consumed by the workbench.
---
## 3. Revision Control Gaps
### 3.1 Critical Gaps
| Gap | Description | Impact | Status |
|-----|-------------|--------|--------|
| ~~**No rollback**~~ | ~~Cannot revert to previous revision~~ | ~~Data recovery difficult~~ | **Implemented** |
| ~~**No comparison**~~ | ~~Cannot diff between revisions~~ | ~~Change tracking manual~~ | **Implemented** |
| **No locking** | No concurrent edit protection | Multi-user unsafe | Open |
| **No approval workflow** | No release/sign-off process | Quality control gap | Open |
### 3.2 Important Gaps
| Gap | Description | Impact | Status |
|-----|-------------|--------|--------|
| **No branching** | Linear history only | No experimental variants | Open |
| ~~**No tagging**~~ | ~~No named milestones~~ | ~~Release tracking manual~~ | **Implemented** (revision labels) |
| ~~**No audit log**~~ | ~~Actions not logged separately~~ | ~~Compliance gap~~ | **Implemented** (migration 009, `audit_log` table + completeness scoring) |
| **Thumbnail missing** | Schema exists, not populated | No visual preview | Open |
### 3.3 Nice-to-Have Gaps
| Gap | Description | Impact |
|-----|-------------|--------|
| **No search** | Cannot search revision comments | Discovery limited |
| **No retention policy** | Revisions never expire | Storage grows unbounded |
| **No delta storage** | Full file per revision | Storage inefficient |
| **No notifications** | No change alerts | Manual monitoring required |
---
## 4. Revision Control Roadmap
### Phase 1: Foundation -- COMPLETE
**Goal:** Enable safe single-user revision management
All Phase 1 items have been implemented:
- **Rollback**: `POST /api/items/{pn}/revisions/{rev}/rollback` - creates new revision from old
- **Revision Comparison**: `GET /api/items/{pn}/revisions/compare?from={rev1}&to={rev2}` - property and file diffs
- **Revision Labels/Status**: `PATCH /api/items/{pn}/revisions/{rev}` - status (draft/review/released/obsolete) and arbitrary labels via migration 007
---
### Phase 2: Multi-User Support
**Goal:** Enable safe concurrent editing
#### 2.1 Pessimistic Locking
```
Effort: High | Priority: High | Risk: Medium | Status: Not Started
```
**Database Migration:**
```sql
CREATE TABLE item_locks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
locked_by TEXT NOT NULL,
locked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
lock_type TEXT NOT NULL DEFAULT 'exclusive',
comment TEXT,
UNIQUE(item_id)
);
CREATE INDEX idx_locks_expires ON item_locks(expires_at);
```
**API Endpoints:**
```
POST /api/items/{pn}/lock # Acquire lock
DELETE /api/items/{pn}/lock # Release lock
GET /api/items/{pn}/lock # Check lock status
```
#### 2.2 Authentication -- COMPLETE
Authentication is fully implemented with three backends (local/bcrypt, LDAP/FreeIPA, OIDC/Keycloak), role-based access control (admin > editor > viewer), API token management, and PostgreSQL-backed sessions. See `docs/AUTH.md` for full details.
- Migration: `009_auth.sql`
- Code: `internal/auth/`, `internal/api/middleware.go`
#### 2.3 Audit Logging -- COMPLETE
Audit logging is implemented via migration 009 with the `audit_log` table and completeness scoring system. Endpoints:
- `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/audit_handlers.go`
---
### Phase 3: Advanced Features
**Goal:** Enhanced revision management capabilities
#### 3.1 Branching Support
```
Effort: High | Priority: Low | Risk: High
```
**Concept:** Named revision streams per item
**Database Migration:**
```sql
CREATE TABLE revision_branches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
name TEXT NOT NULL, -- 'main', 'experimental', 'customer-variant'
base_revision INTEGER, -- Branch point
created_at TIMESTAMPTZ DEFAULT now(),
created_by TEXT,
UNIQUE(item_id, name)
);
ALTER TABLE revisions ADD COLUMN branch_id UUID REFERENCES revision_branches(id);
```
**Complexity:** Requires merge logic, conflict resolution UI
#### 3.2 Release Management
```
Effort: Medium | Priority: Medium | Risk: Low
```
**Database Migration:**
```sql
CREATE TABLE releases (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE, -- 'v1.0', '2026-Q1'
description TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
created_by TEXT,
status TEXT DEFAULT 'draft' -- 'draft', 'released', 'archived'
);
CREATE TABLE release_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
release_id UUID REFERENCES releases(id) ON DELETE CASCADE,
item_id UUID REFERENCES items(id) ON DELETE CASCADE,
revision_number INTEGER NOT NULL,
UNIQUE(release_id, item_id)
);
```
**Use Case:** Snapshot a set of items at specific revisions for a product release
#### 3.3 Thumbnail Generation
```
Effort: Medium | Priority: Low | Risk: Low
```
**Implementation Options:**
1. FreeCAD headless rendering (python script)
2. External service (e.g., CAD thumbnail microservice)
3. User-uploaded thumbnails
**Changes:**
- Add thumbnail generation on file upload
- Store in MinIO at `thumbnails/{part_number}/rev{n}.png`
- Expose via `GET /api/items/{pn}/thumbnail/{rev}`
---
## 5. Recommended Implementation Order
### Completed
1. ~~**Revision Comparison API**~~ - Implemented
2. ~~**Rollback Support**~~ - Implemented
3. ~~**Revision Labels/Status**~~ - Implemented (migration 007)
### Recently Completed
4. ~~**Authentication**~~ - Implemented (3 backends: local, LDAP, OIDC; RBAC; API tokens; sessions)
5. ~~**Audit Logging**~~ - Implemented (audit_log table, completeness scoring)
### Next (Short-term)
6. **Pessimistic Locking** - Required before multi-user
### Medium-term (3-6 Months)
7. **Release Management** - Product milestone tracking
8. **Thumbnail Generation** - Visual preview capability
### Long-term (Future)
10. **Branching** - Complex, defer until needed
11. **Delta Storage** - Optimization, not critical
12. **Notifications** - Nice-to-have workflow enhancement
---
## 6. Migration Considerations
### Part Number Format Migration (Completed)
The recent migration from `XXXXX-CCC-NNNN` to `CCC-NNNN` format has been completed:
- Database migration: `migrations/006_project_tags.sql`
- Schema update: `schemas/kindred-rd.yaml` v3
- Projects: Now many-to-many tags instead of embedded in part number
- File paths: `~/projects/cad/{category}_{name}/{part_number}_{description}.FCStd`
### Future Schema Migrations
The property schema versioning framework (`property_schema_version`, `property_migrations` table) is in place but lacks:
- Automated migration runners
- Rollback capability for failed migrations
- Dry-run validation mode
**Recommendation:** Build migration tooling before adding complex property schemas.
---
## 7. Open Questions (from Specification)
These design decisions remain unresolved:
1. **Property change triggers** - Should editing properties auto-create revision?
2. **Revision metadata editing** - Allow comment updates post-creation?
3. **Soft delete behavior** - Archive or hard delete revisions?
4. **File diff strategy** - Exploded FCStd storage for better diffing?
5. **Retention policy** - How long to keep old revisions?
---
## Appendix A: File Structure
Revision endpoints, status, labels, authentication, audit logging, and file attachments are implemented. Current structure:
```
internal/
api/
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 (78 endpoints)
search.go # Fuzzy search
auth/
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/
001_initial.sql # Core schema
...
011_item_files.sql # Item file attachments (latest)
```
Future features would add:
```
internal/
api/
lock_handlers.go # Locking endpoints
db/
locks.go # Lock repository
releases.go # Release repository
migrations/
012_item_locks.sql # Locking table
013_releases.sql # Release management
```
---
## Appendix B: API Additions Summary
### Phase 1 Endpoints (Implemented)
```
GET /api/items/{pn}/revisions/compare # Diff two revisions
POST /api/items/{pn}/revisions/{rev}/rollback # Create revision from old
PATCH /api/items/{pn}/revisions/{rev} # Update status/labels
```
### Phase 2 Endpoints
**Authentication (Implemented):**
```
GET /api/auth/me # Current user info
GET /api/auth/tokens # List API tokens
POST /api/auth/tokens # Create API token
DELETE /api/auth/tokens/{id} # Revoke API token
```
**Audit (Implemented):**
```
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
DELETE /api/items/{pn}/lock # Release lock
GET /api/items/{pn}/lock # Check lock status
```
### Phase 3 Endpoints (Not Implemented)
```
GET /api/releases # List releases
POST /api/releases # Create release
GET /api/releases/{name} # Get release details
POST /api/releases/{name}/items # Add items to release
GET /api/items/{pn}/thumbnail/{rev} # Get thumbnail
```

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

@@ -0,0 +1,536 @@
# Silo Roadmap
**Version:** 1.1
**Date:** February 2026
**Purpose:** Project inventory, SOLIDWORKS PDM gap analysis, and development roadmap
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Current Project Inventory](#current-project-inventory)
3. [SOLIDWORKS PDM Gap Analysis](#solidworks-pdm-gap-analysis)
4. [Feature Roadmap](#feature-roadmap)
5. [Implementation Phases](#implementation-phases)
---
## Executive Summary
Silo is an R&D-oriented item database and part management system. It provides configurable part number generation, revision tracking, BOM management, and file versioning through MinIO storage. 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)).
This document compares Silo's current capabilities against SOLIDWORKS PDM—the industry-leading product data management solution—to identify gaps and prioritize future development.
### Key Differentiators
| Aspect | Silo | SOLIDWORKS PDM |
|--------|------|----------------|
| **Target CAD** | FreeCAD / Kindred Create (open source) | SOLIDWORKS (proprietary) |
| **Part Numbering** | Schema-as-configuration (YAML) | Fixed format with some customization |
| **Licensing** | Open source / Kindred Proprietary | Commercial ($3,000-$10,000+ per seat) |
| **Storage** | PostgreSQL + MinIO (S3-compatible) | SQL Server + File Archive |
| **Philosophy** | R&D-oriented, lightweight | Enterprise-grade, comprehensive |
---
## Current Project Inventory
### Implemented Features (MVP Complete)
#### Core Database System
- PostgreSQL schema with 13 migrations
- UUID-based identifiers throughout
- Soft delete support via `archived_at` timestamps
- Atomic sequence generation for part numbers
#### Part Number Generation
- YAML schema parser with validation
- Segment types: `string`, `enum`, `serial`, `constant`
- Scope templates for serial counters (e.g., `{category}`, `{project}`)
- Format templates for custom output
#### Item Management
- Full CRUD operations for items
- Item types: part, assembly, drawing, document, tooling, purchased, electrical, software
- Custom properties via JSONB storage
- Project tagging with many-to-many relationships
#### Revision Control
- Append-only revision history
- Revision metadata: properties, file reference, checksum, comment
- Status tracking: draft, review, released, obsolete
- Labels/tags per revision
- Revision comparison (diff)
- Rollback functionality
#### File Management
- MinIO integration with versioning
- File upload/download via REST API
- SHA256 checksums for integrity
- Storage path: `items/{partNumber}/rev{N}.FCStd`
#### Bill of Materials (BOM)
- Relationship types: component, alternate, reference
- Multi-level BOM (recursive expansion with configurable depth)
- Where-used queries (reverse parent lookup)
- BOM CSV and ODS export/import with cycle detection
- Reference designators for electronics
- Quantity tracking with units
- Revision-specific child linking
#### Project Management
- Project CRUD operations
- Unique project codes (2-10 characters)
- Item-to-project tagging
- Project-filtered queries
#### Data Import/Export
- CSV export with configurable properties
- CSV import with dry-run validation
- ODS spreadsheet import/export (items, BOMs, project sheets)
- Template generation for import formatting
#### API & Web Interface
- 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)
- Session management (PostgreSQL-backed, 24h lifetime)
- CSRF protection (nosurf on web forms)
- Middleware: logging, CORS, recovery, request ID
- Web UI — React SPA (Vite + TypeScript, Catppuccin Mocha theme)
- Fuzzy search
- Health and readiness probes
#### Audit & Completeness
- Audit logging (database table with user/action/resource tracking)
- Item completeness scoring with weighted fields
- Category-specific property validation
- Tier classification (critical/low/partial/good/complete)
#### Configuration
- YAML configuration with environment variable overrides
- Multi-schema support
- Docker Compose deployment ready
### Partially Implemented
| Feature | Status | Notes |
|---------|--------|-------|
| Odoo ERP integration | Partial | Config and sync-log CRUD functional; push/pull sync operations are stubs |
| Date segment type | Not started | Schema parser placeholder exists |
| 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 | Partial | 9 Go test files across api, db, ods, partnum, schema packages |
### Infrastructure Status
| Component | Status |
|-----------|--------|
| PostgreSQL | Running (psql.example.internal) |
| MinIO | Configured in Docker Compose |
| Silo API Server | Builds successfully |
| Docker Compose | Complete (dev and production) |
| systemd service | Unit file and env template ready |
| Deployment scripts | setup-host, deploy, init-db, setup-ipa-nginx |
---
## SOLIDWORKS PDM Gap Analysis
This section compares Silo's capabilities against SOLIDWORKS PDM features. Gaps are categorized by priority and implementation complexity.
### Legend
- **Silo Status:** Full / Partial / None
- **Priority:** Critical / High / Medium / Low
- **Complexity:** Simple / Moderate / Complex
---
### 1. Version Control & Revision Management
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Check-in/check-out | Full pessimistic locking | None | High | Moderate |
| Version history | Complete with branching | Full (linear) | - | - |
| Revision labels | A, B, C or custom schemes | Full (custom labels) | - | - |
| Rollback/restore | Full | Full | - | - |
| Compare revisions | Visual + metadata diff | Metadata diff only | Medium | Complex |
| Get Latest Revision | One-click retrieval | Partial (API only) | Medium | Simple |
**Gap Analysis:**
Silo lacks pessimistic locking (check-out), which is critical for multi-user CAD environments where file merging is impractical. Visual diff comparison would require FreeCAD integration for CAD file visualization.
---
### 2. Workflow Management
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Custom workflows | Full visual designer | None | Critical | Complex |
| State transitions | Configurable with permissions | Basic (status field only) | Critical | Complex |
| Parallel approvals | Multiple approvers required | None | High | Complex |
| Automatic transitions | Timer/condition-based | None | Medium | Moderate |
| Email notifications | On state change | None | High | Moderate |
| ECO process | Built-in change management | None | High | Complex |
| Child state conditions | Block parent if children invalid | None | Medium | Moderate |
**Gap Analysis:**
Workflow management is the largest functional gap. SOLIDWORKS PDM offers sophisticated state machines with parallel approvals, automatic transitions, and deep integration with engineering change processes. Silo currently has only a simple status field (draft/review/released/obsolete) with no transition rules or approval processes.
---
### 3. User Management & Security
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| User authentication | Windows AD, LDAP | Full (local, LDAP, OIDC) | - | - |
| Role-based permissions | Granular per folder/state | Partial (3-tier role model) | Medium | Moderate |
| Group management | Full | None | Medium | Moderate |
| Folder permissions | Read/write/delete per folder | None | Medium | Moderate |
| State permissions | Actions allowed per state | None | High | Moderate |
| Audit trail | Complete action logging | Full | - | - |
| Private files | Pre-check-in visibility control | None | Low | Simple |
**Gap Analysis:**
Authentication is implemented with three backends (local, LDAP/FreeIPA, OIDC/Keycloak) and a 3-tier role model (admin > editor > viewer). Audit logging captures user actions. Remaining gaps: group management, folder-level permissions, and state-based permission rules.
---
### 4. Search & Discovery
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Metadata search | Full with custom cards | Partial (API query params + fuzzy) | High | Moderate |
| Full-text content search | iFilters for Office, CAD | None | Medium | Complex |
| Quick search | Toolbar with history | Partial (fuzzy search API) | Medium | Simple |
| Saved searches | User-defined favorites | None | Medium | Simple |
| Advanced operators | AND, OR, NOT, wildcards | None | Medium | Simple |
| Multi-variable search | Search across multiple fields | None | Medium | Simple |
| Where-used search | Find all assemblies using part | Full | - | - |
**Gap Analysis:**
Silo has API-level filtering, fuzzy search, and where-used queries. Remaining gaps: saved searches, advanced search operators, and a richer search UI. Content search (searching within CAD files) is not planned for the server.
---
### 5. BOM Management
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Single-level BOM | Yes | Full | - | - |
| Multi-level BOM | Indented/exploded views | Full (recursive, configurable depth) | - | - |
| BOM comparison | Between revisions | None | Medium | Moderate |
| BOM export | Excel, XML, ERP formats | Full (CSV, ODS) | - | - |
| BOM import | Bulk BOM loading | Full (CSV with upsert) | - | - |
| Calculated BOMs | Quantities rolled up | None | Medium | Moderate |
| Reference designators | Full support | Full | - | - |
| Alternate parts | Substitute tracking | Full | - | - |
**Gap Analysis:**
Multi-level BOM retrieval (recursive CTE with configurable depth) and BOM export (CSV, ODS) are implemented. BOM import supports CSV with upsert and cycle detection. Remaining gap: BOM comparison between revisions.
---
### 6. CAD Integration
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Native CAD add-in | Deep SOLIDWORKS integration | FreeCAD workbench (silo-mod) | Medium | Complex |
| Property mapping | Bi-directional sync | Planned (silo-mod) | Medium | Moderate |
| Task pane | Embedded in CAD UI | Auth dock panel (silo-mod) | Medium | Complex |
| Lightweight components | Handle without full load | N/A | - | - |
| Drawing/model linking | Automatic association | Manual | Medium | Moderate |
| Multi-CAD support | Third-party formats | FreeCAD only | Low | - |
**Gap Analysis:**
CAD integration 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)). The Silo server provides the REST API endpoints consumed by those clients.
---
### 7. External Integrations
| 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 (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 (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.
---
### 8. Reporting & Analytics
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Standard reports | Inventory, usage, activity | None | Medium | Moderate |
| Custom reports | User-defined queries | None | Medium | Moderate |
| Dashboard | Visual KPIs | None | Low | Moderate |
| Export formats | PDF, Excel, CSV | CSV and ODS | Medium | Simple |
**Gap Analysis:**
Reporting capabilities are absent. Basic reports (item counts, revision activity, where-used) would provide immediate value.
---
### 9. File Handling
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| File versioning | Automatic | Full (MinIO) | - | - |
| File preview | Thumbnails, 3D preview | None | Medium | Complex |
| File conversion | PDF, DXF generation | None | Medium | Complex |
| Replication | Multi-site sync | None | Low | Complex |
| File copy with refs | Copy tree with references | None | Medium | Moderate |
**Gap Analysis:**
File storage works well. Thumbnail generation and file preview would significantly improve the web UI experience. Automatic conversion to PDF/DXF is valuable for sharing with non-CAD users.
---
### Gap Summary by Priority
#### Completed (Previously Critical/High)
1. ~~**User authentication**~~ - Implemented: local, LDAP, OIDC
2. ~~**Role-based permissions**~~ - Implemented: 3-tier role model (admin/editor/viewer)
3. ~~**Audit trail**~~ - Implemented: audit_log table with completeness scoring
4. ~~**Where-used search**~~ - Implemented: reverse parent lookup API
5. ~~**Multi-level BOM API**~~ - Implemented: recursive expansion with configurable depth
6. ~~**BOM export**~~ - Implemented: CSV and ODS formats
#### Critical Gaps (Required for Team Use)
1. **Workflow engine** - State machines with transitions and approvals
2. **Check-out locking** - Pessimistic locking for CAD files
#### High Priority Gaps (Significant Value)
1. **Email notifications** - Alert users on state changes
2. **Web UI search** - Advanced search interface with saved searches
3. **Folder/state permissions** - Granular access control beyond role model
#### Medium Priority Gaps (Nice to Have)
1. **Saved searches** - Frequently used queries
2. **File preview/thumbnails** - Visual browsing
3. **Reporting** - Activity and inventory reports
4. **Scheduled tasks** - Background automation
5. **BOM comparison** - Revision diff for assemblies
---
## Feature Roadmap
### Phase 1: Foundation (Current - Q2 2026)
*Complete MVP and stabilize core functionality*
| Feature | Description | Status |
|---------|-------------|--------|
| MinIO integration | File upload/download with versioning and checksums | Complete |
| Revision control | Rollback, comparison, status/labels | Complete |
| CSV import/export | Dry-run validation, template generation | Complete |
| ODS import/export | Items, BOMs, project sheets, templates | Complete |
| Project management | CRUD, many-to-many item tagging | Complete |
| Multi-level BOM | Recursive expansion, where-used, export | Complete |
| Authentication | Local, LDAP, OIDC with role-based access | Complete |
| Audit logging | Action logging, completeness scoring | Complete |
| Unit tests | Core API and database operations | Not Started |
| Date segment type | Support date-based part number segments | Not Started |
| Part number validation | Validate format on creation | Not Started |
| Location CRUD API | Expose location hierarchy via REST | Not Started |
| Inventory API | Expose inventory operations via REST | Not Started |
### Phase 2: Multi-User (Q2-Q3 2026)
*Enable team collaboration*
| Feature | Description | Status |
|---------|-------------|--------|
| LDAP authentication | Integrate with FreeIPA/Active Directory | **Complete** |
| OIDC authentication | Keycloak / OpenID Connect | **Complete** |
| Audit logging | Record all user actions with timestamps | **Complete** |
| Session management | Token-based and session-based API authentication | **Complete** |
| User/group management | Create, assign, manage users and groups | Not Started |
| Folder permissions | Read/write/delete per folder hierarchy | Not Started |
| Check-out locking | Pessimistic locks with timeout | Not Started |
### Phase 3: Workflow Engine (Q3-Q4 2026)
*Implement engineering change processes*
| Feature | Description | Complexity |
|---------|-------------|------------|
| Workflow designer | YAML-defined state machines | Complex |
| State transitions | Configurable transition rules | Complex |
| Transition permissions | Who can execute which transitions | Moderate |
| Single approvals | Basic approval workflow | Moderate |
| Parallel approvals | Multi-approver gates | Complex |
| Automatic transitions | Timer and condition-based | Complex |
| Email notifications | SMTP integration for alerts | Simple |
| Child state conditions | Block parent transitions | Moderate |
### Phase 4: Search & Discovery (Q4 2026 - Q1 2027)
*Improve findability and navigation*
| Feature | Description | Status |
|---------|-------------|--------|
| Where-used queries | Find parent assemblies | **Complete** |
| Fuzzy search | Quick search across items | **Complete** |
| Advanced search UI | Web interface with filters | Not Started |
| Search operators | AND, OR, NOT, wildcards | Not Started |
| Saved searches | User favorites | Not Started |
| Content search | Search within file content | Not Started |
### Phase 5: BOM & Reporting (Q1-Q2 2027)
*Enhanced BOM management and analytics*
| Feature | Description | Status |
|---------|-------------|--------|
| Multi-level BOM API | Recursive assembly retrieval | **Complete** |
| BOM export | CSV and ODS formats | **Complete** |
| BOM import | CSV with upsert and cycle detection | **Complete** |
| BOM comparison | Diff between revisions | Not Started |
| Standard reports | Activity, inventory, usage | Not Started |
| Custom queries | User-defined report builder | Not Started |
| Dashboard | Visual KPIs and metrics | Not Started |
### Phase 6: Advanced Features (Q2-Q4 2027)
*Enterprise capabilities*
| Feature | Description | Complexity |
|---------|-------------|------------|
| File preview | Thumbnail generation | Complex |
| File conversion | Auto-generate PDF/DXF | Complex |
| ERP integration | Adapter framework | Complex |
| Task scheduler | Background job processing | Moderate |
| Webhooks | Event notifications to external systems | Moderate |
| API rate limiting | Protect against abuse | Simple |
---
## Implementation Phases
### Phase 1 Detailed Tasks
#### 1.1 MinIO Integration -- COMPLETE
- [x] MinIO service configured in Docker Compose
- [x] File upload via REST API
- [x] File download via REST API (latest and by revision)
- [x] SHA256 checksums on upload
#### 1.2 Authentication & Authorization -- COMPLETE
- [x] Local authentication (bcrypt)
- [x] LDAP/FreeIPA authentication
- [x] OIDC/Keycloak authentication
- [x] Role-based access control (admin/editor/viewer)
- [x] API token management (SHA-256 hashed)
- [x] Session management (PostgreSQL-backed)
- [x] CSRF protection (nosurf)
- [x] Audit logging (database table)
#### 1.3 Multi-level BOM & Export -- COMPLETE
- [x] Recursive BOM expansion with configurable depth
- [x] Where-used reverse lookup
- [x] BOM CSV export/import with cycle detection
- [x] BOM ODS export
- [x] ODS item export/import/template
#### 1.4 Unit Test Suite
- [ ] Database connection and transaction tests
- [ ] Item CRUD operation tests
- [ ] Revision creation and retrieval tests
- [ ] Part number generation tests
- [ ] File upload/download tests
- [ ] CSV import/export tests
- [ ] API endpoint tests
#### 1.5 Missing Segment Types
- [ ] Implement date segment type
- [ ] Add strftime-style format support
#### 1.6 Location & Inventory APIs
- [ ] `GET /api/locations` - List locations
- [ ] `POST /api/locations` - Create location
- [ ] `GET /api/locations/{path}` - Get location
- [ ] `DELETE /api/locations/{path}` - Delete location
- [ ] `GET /api/inventory/{partNumber}` - Get inventory
- [ ] `POST /api/inventory/{partNumber}/adjust` - Adjust quantity
- [ ] `POST /api/inventory/{partNumber}/move` - Move between locations
---
## Success Metrics
### Phase 1 (Foundation)
- All existing tests pass
- File upload/download works end-to-end
- FreeCAD users can checkout, modify, commit parts
### Phase 2 (Multi-User)
- 5+ concurrent users supported
- No data corruption under concurrent access
- Audit log captures all modifications
### Phase 3 (Workflow)
- Engineering change process completable in Silo
- Email notifications delivered reliably
- Workflow state visible in web UI
### Phase 4+ (Advanced)
- Search returns results in <2 seconds
- Where-used queries complete in <5 seconds
- BOM export matches assembly structure
---
## References
### SOLIDWORKS PDM Documentation
- [SOLIDWORKS PDM Product Page](https://www.solidworks.com/product/solidworks-pdm)
- [What's New in SOLIDWORKS PDM 2025](https://blogs.solidworks.com/solidworksblog/2024/10/whats-new-in-solidworks-pdm-2025.html)
- [Top 5 Enhancements in SOLIDWORKS PDM 2024](https://blogs.solidworks.com/solidworksblog/2023/10/top-5-enhancements-in-solidworks-pdm-2024.html)
- [SOLIDWORKS PDM Workflow Transitions](https://help.solidworks.com/2023/english/EnterprisePDM/Admin/c_workflow_transition.htm)
- [Ultimate Guide to SOLIDWORKS PDM Permissions](https://www.goengineer.com/blog/ultimate-guide-to-solidworks-pdm-permissions)
- [Searching in SOLIDWORKS PDM](https://help.solidworks.com/2021/english/EnterprisePDM/fileexplorer/c_searches.htm)
- [SOLIDWORKS PDM API Getting Started](https://3dswym.3dexperience.3ds.com/wiki/solidworks-news-info/getting-started-with-the-solidworks-pdm-api-solidpractices_gBCYaM75RgORBcpSO1m_Mw)
### Silo Documentation
- [Specification](docs/SPECIFICATION.md)
- [Development Status](docs/STATUS.md)
- [Deployment Guide](docs/DEPLOYMENT.md)
- [Gap Analysis](docs/GAP_ANALYSIS.md)
---
## Appendix: Feature Comparison Matrix
| Category | Feature | SW PDM Standard | SW PDM Pro | Silo Current | Silo Planned |
|----------|---------|-----------------|------------|--------------|--------------|
| **Version Control** | Check-in/out | Yes | Yes | No | Phase 2 |
| | Version history | Yes | Yes | Yes | - |
| | Rollback | Yes | Yes | Yes | - |
| | Revision labels/status | Yes | Yes | Yes | - |
| | Revision comparison | Yes | Yes | Yes (metadata) | - |
| **Workflow** | Custom workflows | Limited | Yes | No | Phase 3 |
| | Parallel approval | No | Yes | No | Phase 3 |
| | Notifications | No | Yes | No | Phase 3 |
| **Security** | User auth | Windows | Windows/LDAP | Yes (local, LDAP, OIDC) | - |
| | Permissions | Basic | Granular | Partial (role-based) | Phase 2 |
| | Audit trail | Basic | Full | Yes | - |
| **Search** | Metadata search | Yes | Yes | Partial (API + fuzzy) | Phase 4 |
| | Content search | No | Yes | No | Phase 4 |
| | Where-used | Yes | Yes | Yes | - |
| **BOM** | Single-level | Yes | Yes | Yes | - |
| | Multi-level | Yes | Yes | Yes (recursive) | - |
| | BOM export | Yes | Yes | Yes (CSV, ODS) | - |
| **Data** | CSV import/export | Yes | Yes | Yes | - |
| | ODS import/export | No | No | Yes | - |
| | Project management | Yes | Yes | Yes | - |
| **Integration** | API | Limited | Full | Full REST (75) | - |
| | ERP connectors | No | Yes | Partial (Odoo stubs) | Phase 6 |
| | 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

@@ -0,0 +1,948 @@
# Silo: Item Database and Part Management System
**Version:** 0.2
**Date:** February 2026
**Author:** Kindred Systems LLC
---
## 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 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
Silo treats **part numbering schemas as configuration, not code**. Multiple numbering schemes can coexist, each defined in YAML. The system is schema-agnostic—it doesn't impose a particular part numbering philosophy (intelligent vs. non-intelligent numbers) but instead provides the machinery to implement whatever scheme the organization requires.
### 1.2 Key Principles
- **Items are the atomic unit**: Everything is an item (parts, assemblies, drawings, documents)
- **Schemas are mutable**: Part numbering schemas can evolve, though migration tooling is out of scope for MVP
- **Append-only history**: All parameter changes are recorded; item state is reconstructable at any point in time
- **Configuration over convention**: Hierarchies, relationships, and behaviors are YAML-defined
---
## 2. Architecture
### 2.1 Components
```
┌─────────────────────────────────────────────────────────────┐
│ CAD Clients (silo-mod, silo-calc) │
│ FreeCAD Workbench · LibreOffice Calc Extension │
│ (maintained in separate repositories) │
└─────────────────────────────────────────────────────────────┘
│ REST API
┌─────────────────────────────────────────────────────────────┐
│ Silo Server (silod) │
│ - REST API (78 endpoints) │
│ - Authentication (local, LDAP, OIDC) │
│ - Schema parsing and validation │
│ - Part number generation engine │
│ - Revision management │
│ - Relationship graph / BOM │
│ - Web UI (React SPA) │
└─────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ PostgreSQL │ │ MinIO │
│ (psql.example.internal)│ │ - File storage │
│ - Item metadata │ │ - Versioned objects │
│ - Relationships │ │ - Thumbnails │
│ - Revision history │ │ │
│ - Auth / Sessions │ │ │
│ - Audit log │ │ │
└─────────────────────────┘ └─────────────────────────────┘
```
### 2.2 Technology Stack
| Component | Technology | Notes |
|-----------|------------|-------|
| 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 |
| Sessions | PostgreSQL pgxstore | alexedwards/scs, 24h lifetime |
| Web UI | React 19, Vite 6, TypeScript 5.7 | Catppuccin Mocha theme, inline styles |
---
## 3. Data Model
### 3.1 Items
An **item** is the fundamental entity. Items have:
- A **part number** (generated according to a schema)
- A **type** (part, assembly, drawing, document, etc.)
- **Properties** (key-value pairs, schema-defined and custom)
- **Relationships** to other items
- **Revisions** (append-only history)
- **Files** (optional, stored in MinIO)
- **Location** (optional physical inventory location)
### 3.2 Database Schema (Conceptual)
```sql
-- Part numbering schemas (YAML stored as text, parsed at runtime)
CREATE TABLE schemas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
definition JSONB NOT NULL, -- parsed YAML
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Items (core entity)
CREATE TABLE items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
part_number TEXT UNIQUE NOT NULL,
schema_id UUID REFERENCES schemas(id),
item_type TEXT NOT NULL, -- 'part', 'assembly', 'drawing', etc.
created_at TIMESTAMPTZ DEFAULT now(),
current_revision_id UUID -- points to latest revision
);
-- Append-only revision history
CREATE TABLE revisions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID REFERENCES items(id) NOT NULL,
revision_number INTEGER NOT NULL,
properties JSONB NOT NULL, -- all properties at this revision
file_version TEXT, -- MinIO version ID if applicable
created_at TIMESTAMPTZ DEFAULT now(),
created_by TEXT, -- user identifier (future: LDAP DN)
comment TEXT,
UNIQUE(item_id, revision_number)
);
-- Item relationships (BOM structure)
CREATE TABLE relationships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_item_id UUID REFERENCES items(id) NOT NULL,
child_item_id UUID REFERENCES items(id) NOT NULL,
relationship_type TEXT NOT NULL, -- 'component', 'alternate', 'reference'
quantity DECIMAL,
reference_designator TEXT, -- e.g., "R1", "C3" for electronics
metadata JSONB, -- assembly-specific relationship config
revision_id UUID REFERENCES revisions(id), -- which revision this applies to
created_at TIMESTAMPTZ DEFAULT now()
);
-- Location hierarchy (configurable via YAML)
CREATE TABLE locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
path TEXT UNIQUE NOT NULL, -- e.g., "lab/shelf-a/bin-3"
name TEXT NOT NULL,
parent_id UUID REFERENCES locations(id),
location_type TEXT NOT NULL, -- defined in location schema
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Item inventory (quantity at location)
CREATE TABLE inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID REFERENCES items(id) NOT NULL,
location_id UUID REFERENCES locations(id) NOT NULL,
quantity DECIMAL NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(item_id, location_id)
);
-- Sequence counters for part number generation
CREATE TABLE sequences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
schema_id UUID REFERENCES schemas(id),
scope TEXT NOT NULL, -- scope key (e.g., project code, type code)
current_value INTEGER NOT NULL DEFAULT 0,
UNIQUE(schema_id, scope)
);
```
---
## 4. YAML Configuration System
### 4.1 Part Numbering Schema
Schemas define how part numbers are generated. Each schema consists of **segments** that are concatenated with a **separator**.
```yaml
# /etc/silo/schemas/kindred-rd.yaml
schema:
name: kindred-rd
version: 3
description: "Kindred Systems R&D part numbering"
# Separator between segments (default: "-")
separator: "-"
# Uniqueness enforcement
uniqueness:
scope: global
case_sensitive: false
segments:
- name: category
type: enum
description: "Category code (2-3 characters)"
required: true
values:
F01: "Hex Cap Screw"
F02: "Socket Head Cap Screw"
# ... 70+ categories across:
# F01-F18: Fasteners
# C01-C17: Fluid Fittings
# R01-R44: Motion Components
# S01-S17: Structural Materials
# E01-E27: Electrical Components
# M01-M18: Mechanical Components
# T01-T08: Tooling and Fixtures
# A01-A07: Assemblies
# P01-P05: Purchased/Off-the-Shelf
# X01-X08: Custom Fabricated Parts
- name: sequence
type: serial
length: 4
padding: "0"
start: 1
description: "Sequential number within category"
scope: "{category}"
format: "{category}-{sequence}"
# Example outputs:
# F01-0001 (first hex cap screw)
# R27-0001 (first linear rail)
# A01-0001 (first assembly)
```
> **Note:** The schema was migrated from a `{project}-{type}-{sequence}` format (v1) to `{category}-{sequence}` (v3). Projects are now managed as many-to-many tags on items rather than embedded in the part number. See `migrations/006_project_tags.sql`.
### 4.2 Segment Types
| Type | Description | Options |
|------|-------------|---------|
| `string` | Fixed or variable length string | `length`, `min_length`, `max_length`, `pattern`, `case` |
| `enum` | Predefined set of values | `values` (map of code → description) |
| `serial` | Auto-incrementing integer | `length`, `padding`, `start`, `scope` |
| `date` | Date-based segment | `format` (strftime-style) |
| `constant` | Fixed value | `value` |
### 4.3 Serial Scope Templates
The `scope` field in serial segments supports template variables referencing other segments:
```yaml
# Sequence per category (current kindred-rd schema)
scope: "{category}"
# Global sequence (no scope)
scope: null
```
### 4.4 Alternative Schema Example (Simple Sequential)
```yaml
# /etc/silo/schemas/simple.yaml
schema:
name: simple
version: 1
description: "Simple non-intelligent numbering"
segments:
- name: prefix
type: constant
value: "P"
- name: sequence
type: serial
length: 6
padding: "0"
scope: null # global counter
format: "{prefix}{sequence}"
separator: ""
# Output: P000001, P000002, ...
```
### 4.5 Location Hierarchy Schema
```yaml
# /etc/silo/schemas/locations.yaml
location_schema:
name: kindred-lab
version: 1
hierarchy:
- level: 0
type: facility
name_pattern: "^[a-z-]+$"
- level: 1
type: area
name_pattern: "^[a-z-]+$"
- level: 2
type: shelf
name_pattern: "^shelf-[a-z]$"
- level: 3
type: bin
name_pattern: "^bin-[0-9]+$"
# Path format
path_separator: "/"
# Example paths:
# lab/main-area/shelf-a/bin-1
# lab/storage/shelf-b/bin-12
```
### 4.6 Assembly Metadata Schema
Each assembly can define its own relationship tracking behavior:
```yaml
# Stored in item properties or as a linked document
assembly_config:
# What relationship types this assembly uses
relationship_types:
- component # standard BOM entry
- alternate # interchangeable substitute
- reference # related but not part of BOM
# Whether to track reference designators
use_reference_designators: true
designator_format: "^[A-Z]+[0-9]+$" # e.g., R1, C3, U12
# Revision linking behavior
child_revision_tracking: specific # or "latest"
# Custom properties for relationships
relationship_properties:
- name: mounting_orientation
type: enum
values: [top, bottom, left, right, front, back]
- name: notes
type: text
```
---
## 5. Client Integration
CAD workbench and spreadsheet extension implementations are maintained in separate repositories ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)). The Silo server provides the REST API endpoints consumed by those clients.
### 5.1 File Storage Strategy
Files are stored as whole objects in MinIO with versioning enabled. Storage path convention: `items/{partNumber}/rev{N}.ext`. SHA-256 checksums are captured on upload for integrity verification.
Future option: exploded storage (unpack ZIP-based CAD archives for better diffing).
### 5.2 Checkout Locking (Future)
Future multi-user support will need a server-side locking strategy:
- **Pessimistic locking**: Checkout acquires exclusive lock
- **Optimistic locking**: Allow concurrent edits, handle conflicts on commit
Recommendation: Pessimistic locking for CAD files (merge is impractical).
---
## 6. Web Interface
### 6.1 Architecture
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/*`.
- **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:
```
silo://open/PROTO-AS-0001 # Open latest revision
silo://open/PROTO-AS-0001?rev=3 # Open specific revision
```
See [frontend-spec.md](../frontend-spec.md) for full component specifications.
---
## 7. Revision Tracking
### 7.1 Append-Only Model
Every property change creates a new revision record. The current state is always the latest revision, but any historical state can be reconstructed.
```
Item: PROTO-AS-0001
Revision 1 (2026-01-15): Initial creation
- description: "Main chassis assembly"
- material: null
- weight: null
Revision 2 (2026-01-20): Updated properties
- description: "Main chassis assembly"
- material: "6061-T6 Aluminum"
- weight: 2.5
Revision 3 (2026-02-01): Design change
- description: "Main chassis assembly v2"
- material: "6061-T6 Aluminum"
- weight: 2.3
```
### 7.2 Revision Creation
Revisions are created explicitly by user action (not automatic):
- `silo commit` from FreeCAD
- "Save Revision" button in web UI
- API call with explicit revision flag
### 7.3 Revision vs. File Version
- **Revision**: Silo metadata revision (tracked in PostgreSQL)
- **File Version**: MinIO object version (automatic on upload)
A single Silo revision may span multiple file uploads during editing. Only committed revisions create formal revision records.
---
## 8. Relationships and BOM
### 8.1 Relationship Types
| Type | Description | Use Case |
|------|-------------|----------|
| `component` | Part is used in assembly | Standard BOM entry |
| `alternate` | Interchangeable substitute | Alternative sourcing |
| `reference` | Related item, not in BOM | Drawings, specs, tools |
### 8.2 Reference Designators
For assemblies that require them (electronics, complex mechanisms):
```yaml
# Relationship record
parent: PROTO-AS-0001
child: PROTO-PT-0042
type: component
quantity: 4
reference_designators: ["R1", "R2", "R3", "R4"]
```
### 8.3 Revision-Specific Relationships
Relationships can link to specific child revisions or track latest:
```yaml
# Locked to specific revision
child: PROTO-PT-0042
child_revision: 3
# Always use latest (default for R&D)
child: PROTO-PT-0042
child_revision: null # means "latest"
```
Assembly metadata YAML controls default behavior per assembly.
### 8.4 Flat BOM and Assembly Costing
Two endpoints provide procurement- and manufacturing-oriented views of the BOM:
**Flat BOM** (`GET /api/items/{partNumber}/bom/flat`) walks the full assembly tree and returns a consolidated list of **leaf parts only** (parts with no BOM children). Quantities are multiplied through each nesting level and duplicate parts are summed.
```
Assembly A (qty 1)
├── Sub-assembly B (qty 2)
│ ├── Part X (qty 3) → total 6
│ └── Part Y (qty 1) → total 2
└── Part Z (qty 4) → total 4
```
Response:
```json
{
"part_number": "A",
"flat_bom": [
{ "part_number": "X", "description": "...", "total_quantity": 6 },
{ "part_number": "Y", "description": "...", "total_quantity": 2 },
{ "part_number": "Z", "description": "...", "total_quantity": 4 }
]
}
```
**Assembly Cost** (`GET /api/items/{partNumber}/bom/cost`) builds on the flat BOM and multiplies each leaf's `total_quantity` by its `standard_cost` to produce per-line extended costs and a total assembly cost.
```json
{
"part_number": "A",
"total_cost": 124.50,
"cost_breakdown": [
{ "part_number": "X", "total_quantity": 6, "unit_cost": 10.00, "extended_cost": 60.00 },
{ "part_number": "Y", "total_quantity": 2, "unit_cost": 7.25, "extended_cost": 14.50 },
{ "part_number": "Z", "total_quantity": 4, "unit_cost": 12.50, "extended_cost": 50.00 }
]
}
```
Both endpoints detect BOM cycles and return **HTTP 409** with the offending path:
```json
{ "error": "cycle_detected", "detail": "BOM cycle detected: A → B → A" }
```
---
## 9. Physical Inventory
### 9.1 Location Management
Locations are hierarchical, defined by YAML schema. Each item can exist at multiple locations with quantities.
```
Location: lab/main-area/shelf-a/bin-3
- PROTO-PT-0001: 15 units
- PROTO-PT-0002: 8 units
Location: lab/storage/shelf-b/bin-1
- PROTO-PT-0001: 50 units (spare stock)
```
### 9.2 Inventory Operations
- **Add**: Increase quantity at location
- **Remove**: Decrease quantity at location
- **Move**: Transfer between locations
- **Adjust**: Set absolute quantity (for cycle counts)
All operations logged for audit trail (future consideration).
---
## 10. Authentication
Silo supports three authentication backends that can be enabled independently or combined. When authentication is disabled (`auth.enabled: false`), all routes are open and a synthetic dev user with the `admin` role is injected into every request.
### 10.1 Backends
| Backend | Use Case | Config Key |
|---------|----------|------------|
| **Local** | Username/password stored in database (bcrypt cost 12) | `auth.local` |
| **LDAP** | FreeIPA / Active Directory via LDAP bind | `auth.ldap` |
| **OIDC** | Keycloak or any OpenID Connect provider (redirect flow) | `auth.oidc` |
### 10.2 Role Model
Three roles with a strict hierarchy: `admin > editor > viewer`
| Permission | viewer | editor | admin |
|-----------|--------|--------|-------|
| Read items, projects, schemas, BOMs | Yes | Yes | Yes |
| Create/update items and revisions | No | Yes | Yes |
| Upload files, manage BOMs | No | Yes | Yes |
| Import CSV/ODS | No | Yes | Yes |
| Manage own API tokens | Yes | Yes | Yes |
| User management (future) | No | No | Yes |
### 10.3 API Tokens
Raw token format: `silo_` + 64 hex characters (32 random bytes from `crypto/rand`). Only the SHA-256 hash is stored in the database. Tokens inherit the owning user's role.
### 10.4 Sessions
PostgreSQL-backed sessions via `alexedwards/scs` pgxstore. Cookie: `silo_session`, HttpOnly, SameSite=Lax, 24h lifetime. `Secure` flag is set when `auth.enabled` is true.
See [AUTH.md](AUTH.md) for full architecture details and [AUTH_USER_GUIDE.md](AUTH_USER_GUIDE.md) for setup instructions.
---
## 11. API Design
### 11.1 REST Endpoints (78 Implemented)
```
# Health (no auth)
GET /health # Basic health check
GET /ready # Readiness (DB + MinIO)
# Auth (no auth required)
GET /login # Login page
POST /login # Login form handler
POST /logout # Logout
GET /auth/oidc # OIDC login redirect
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
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
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]
# Projects (read: viewer, write: editor)
GET /api/projects # List projects
GET /api/projects/{code} # Get project
GET /api/projects/{code}/items # Get project items
GET /api/projects/{code}/sheet.ods # Export project sheet as ODS
POST /api/projects # Create project [editor]
PUT /api/projects/{code} # Update project [editor]
DELETE /api/projects/{code} # Delete project [editor]
# 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
GET /api/items/template.ods # ODS import template
POST /api/items # Create item [editor]
POST /api/items/import # Import items from CSV [editor]
POST /api/items/import.ods # Import items from ODS [editor]
# Item Detail
GET /api/items/{partNumber} # Get item details
PUT /api/items/{partNumber} # Update item [editor]
DELETE /api/items/{partNumber} # Archive item [editor]
# Item-Project Tags
GET /api/items/{partNumber}/projects # Get item's projects
POST /api/items/{partNumber}/projects # Add project tags [editor]
DELETE /api/items/{partNumber}/projects/{code} # Remove project tag [editor]
# Revisions
GET /api/items/{partNumber}/revisions # List revisions
GET /api/items/{partNumber}/revisions/compare # Compare two revisions
GET /api/items/{partNumber}/revisions/{revision} # Get specific revision
POST /api/items/{partNumber}/revisions # Create revision [editor]
PATCH /api/items/{partNumber}/revisions/{revision} # Update status/labels [editor]
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
GET /api/items/{partNumber}/bom/expanded # Multi-level BOM (recursive)
GET /api/items/{partNumber}/bom/flat # Flattened BOM (leaf parts, rolled-up quantities)
GET /api/items/{partNumber}/bom/cost # Assembly cost roll-up
GET /api/items/{partNumber}/bom/where-used # Where-used (parent lookup)
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]
# Audit (viewer)
GET /api/audit/completeness # Item completeness scores
GET /api/audit/completeness/{partNumber} # Item detail breakdown
# Integrations — Odoo (read: viewer, write: editor)
GET /api/integrations/odoo/config # Get Odoo configuration
GET /api/integrations/odoo/sync-log # Get sync history
PUT /api/integrations/odoo/config # Update Odoo config [editor]
POST /api/integrations/odoo/test-connection # Test connection [editor] (stub)
POST /api/integrations/odoo/sync/push/{partNumber} # Push to Odoo [editor] (stub)
POST /api/integrations/odoo/sync/pull/{odooId} # Pull from Odoo [editor] (stub)
# Sheets (editor)
POST /api/sheets/diff # Diff ODS sheet against DB [editor]
# Part Number Generation (editor)
POST /api/generate-part-number # Generate without creating item [editor]
```
### 11.2 Not Yet Implemented
The following endpoints from the original design are not yet implemented:
```
# Locations (tables exist, no API handlers)
GET /api/locations
POST /api/locations
GET /api/locations/{path}
DELETE /api/locations/{path}
# Inventory (tables exist, no API handlers)
GET /api/inventory/{partNumber}
POST /api/inventory/{partNumber}/adjust
POST /api/inventory/{partNumber}/move
```
---
## 12. MVP Scope
### 12.1 Implemented
- [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 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)
- [x] Where-used queries (reverse parent lookup)
- [x] Flat BOM flattening with quantity roll-up and cycle detection
- [x] Assembly cost roll-up using standard_cost
- [x] BOM CSV and ODS export/import
- [x] Reference designator tracking
- [x] Revision history (append-only) with rollback and comparison
- [x] Revision status and labels
- [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, 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)
- [x] Audit logging and completeness scoring
- [x] CSRF protection (nosurf)
- [x] Fuzzy search
- [x] Property schema versioning framework
- [x] Docker Compose deployment (dev and prod)
- [x] systemd service and deployment scripts
### 12.2 Partially Implemented
- [ ] Location hierarchy (database tables exist, no API endpoints)
- [ ] Inventory tracking (database tables exist, no API endpoints)
- [ ] Date segment type (schema parser placeholder only)
- [ ] Part number format validation on creation
- [ ] Odoo ERP integration (config and sync-log functional; push/pull are stubs)
### 12.3 Not Started
- [ ] Unit tests (Go server — 9 test files exist, coverage is partial)
- [ ] Schema migration tooling
- [ ] Checkout locking
- [ ] Approval workflows
- [ ] Exploded file storage with diffing
- [ ] Notifications
- [ ] Reporting/analytics
---
## 13. Open Questions
1. **Thumbnail generation**: Generate thumbnails from CAD files on commit? Useful for web UI browsing.
2. **Search indexing**: PostgreSQL full-text search sufficient, or add dedicated search (Meilisearch, etc.)?
3. **Checkout locking**: Pessimistic vs optimistic locking strategy for multi-user CAD file editing.
---
## 14. References
### 14.1 Design Influences
- **CycloneDX BOM specification**: JSON/YAML schema patterns for component identification, relationships, and metadata (https://cyclonedx.org)
- **OpenBOM data model**: Reference-instance separation, flexible property schemas
- **Ansible inventory YAML**: Hierarchical configuration patterns with variable inheritance
### 14.2 Related Standards
- **ISO 10303 (STEP)**: Product data representation
- **IPC-2581**: Electronics assembly BOM format
- **Package URL (PURL)**: Standardized component identification
---
## Appendix A: Example YAML Files
### A.1 Complete Part Numbering Schema
See `schemas/kindred-rd.yaml` for the full schema (v3). Summary:
```yaml
# kindred-rd-schema.yaml (abbreviated)
schema:
name: kindred-rd
version: 3
description: "Kindred Systems R&D part numbering"
separator: "-"
uniqueness:
scope: global
case_sensitive: false
segments:
- name: category
type: enum
description: "Category code"
required: true
values:
F01: "Hex Cap Screw"
F02: "Socket Head Cap Screw"
# ... 70+ categories (see full file)
- name: sequence
type: serial
length: 4
padding: "0"
start: 1
description: "Sequential number within category"
scope: "{category}"
format: "{category}-{sequence}"
# Example outputs: F01-0001, R27-0001, A01-0001
```
### A.2 Complete Location Schema
```yaml
# kindred-locations.yaml
location_schema:
name: kindred-lab
version: 1
description: "Kindred Systems lab and storage locations"
path_separator: "/"
hierarchy:
- level: 0
type: facility
description: "Building or site"
name_pattern: "^[a-z][a-z0-9-]*$"
examples: ["lab", "warehouse", "office"]
- level: 1
type: area
description: "Room or zone within facility"
name_pattern: "^[a-z][a-z0-9-]*$"
examples: ["main-lab", "storage", "assembly"]
- level: 2
type: shelf
description: "Shelving unit"
name_pattern: "^shelf-[a-z]$"
examples: ["shelf-a", "shelf-b"]
- level: 3
type: bin
description: "Individual container or bin"
name_pattern: "^bin-[0-9]{1,3}$"
examples: ["bin-1", "bin-42", "bin-100"]
# Properties tracked per location type
properties:
facility:
- name: address
type: text
required: false
area:
- name: climate_controlled
type: boolean
default: false
shelf:
- name: max_weight_kg
type: number
required: false
bin:
- name: bin_size
type: enum
values: [small, medium, large]
default: medium
```
### A.3 Assembly Configuration
```yaml
# Stored as item property or linked document
# Example: assembly PROTO-AS-0001
assembly_config:
name: "Main Chassis Assembly"
relationship_types:
- component
- alternate
- reference
use_reference_designators: false
child_revision_tracking: latest
# Assembly-specific BOM properties
relationship_properties:
- name: installation_notes
type: text
- name: torque_spec
type: text
- name: adhesive_required
type: boolean
default: false
# Validation rules
validation:
require_quantity: true
min_components: 1
```

View File

@@ -0,0 +1,98 @@
# Silo Development Status
**Last Updated:** 2026-02-08
---
## Implementation Status
### Core Systems
| Component | Status | Notes |
|-----------|--------|-------|
| 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 | 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 |
| Project management | Complete | CRUD, many-to-many item tagging |
| CSV import/export | Complete | Dry-run validation, template generation |
| ODS import/export | Complete | Items, BOMs, project sheets, templates |
| Multi-level BOM | Complete | Recursive expansion, where-used, CSV/ODS export/import |
| Authentication | Complete | Local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak |
| Role-based access control | Complete | admin > editor > viewer hierarchy |
| API token management | Complete | SHA-256 hashed, bearer auth |
| Session management | Complete | PostgreSQL-backed (pgxstore), 24h lifetime |
| Audit logging | Complete | audit_log table, completeness scoring |
| CSRF protection | Complete | nosurf on web forms |
| Fuzzy search | Complete | sahilm/fuzzy library |
| 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 |
| systemd service | Complete | Unit file and environment template |
### Client Integrations
FreeCAD workbench and LibreOffice Calc extension are maintained in separate repositories ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)). The server provides the REST API and ODS endpoints consumed by those clients.
### Not Yet Implemented
| Feature | Notes |
|---------|-------|
| Location API endpoints | Database tables exist (`locations`, `inventory`), no REST handlers |
| 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 | 9 Go test files across api, db, ods, partnum, schema packages |
---
## Infrastructure
| Service | Host | Status |
|---------|------|--------|
| PostgreSQL | psql.example.internal:5432 | Running |
| MinIO | localhost:9000 (API) / :9001 (console) | Configured |
| Silo API | localhost:8080 | Builds successfully |
---
## Schema Status
The part numbering schema (`kindred-rd`) is at **version 3** using the `{category}-{sequence}` format (e.g., `F01-0001`). This replaced the earlier `{project}-{type}-{sequence}` format. Projects are now managed as many-to-many tags rather than being embedded in part numbers.
The schema defines 170 category codes across 10 groups:
- F01-F18: Fasteners
- C01-C17: Fluid Fittings
- R01-R44: Motion Components
- S01-S17: Structural Materials
- E01-E27: Electrical Components
- M01-M18: Mechanical Components
- T01-T08: Tooling and Fixtures
- A01-A07: Assemblies
- P01-P05: Purchased/Off-the-Shelf
- X01-X08: Custom Fabricated Parts
---
## Database Migrations
| Migration | Description |
|-----------|-------------|
| 001_initial.sql | Core schema (items, revisions, relationships, locations, inventory, sequences) |
| 002_sequence_by_name.sql | Sequence naming changes |
| 003_remove_material.sql | Schema cleanup |
| 004_cad_sync_state.sql | CAD synchronization state |
| 005_property_schema_version.sql | Property versioning framework |
| 006_project_tags.sql | Many-to-many project-item relationships |
| 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, 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

@@ -0,0 +1,757 @@
# Silo Frontend Specification
Current as of 2026-02-11. Documents the React + Vite + TypeScript frontend (migration from Go templates is complete).
## Overview
The Silo web UI has been migrated from server-rendered Go templates to a React single-page application. The Go templates (~7,000 lines across 7 files) have been removed. The Go API server serves JSON at `/api/*` and the React SPA at `/`.
**Stack**: React 19, React Router 7, Vite 6, TypeScript 5.7
**Theme**: Catppuccin Mocha (dark) via CSS custom properties
**Styling**: Inline React styles using `React.CSSProperties` — no CSS modules, no Tailwind, no styled-components
**State**: Local `useState` + custom hooks. No global state library (no Redux, Zustand, etc.)
**Dependencies**: Minimal — only `react`, `react-dom`, `react-router-dom`. No axios, no tanstack-query.
## Migration Status
| Phase | Issue | Title | Status |
|-------|-------|-------|--------|
| 1 | #7 | Scaffold React + Vite + TS, shared layout, auth, API client | Code complete |
| 2 | #8 | Migrate Items page with UI improvements | Code complete |
| 3 | #9 | Migrate Projects, Schemas, Settings, Login pages | Code complete |
| 4 | #10 | Remove Go templates, Docker integration, cleanup | Complete |
## Architecture
```
Browser
└── React SPA (served at /)
├── Vite dev server (development) → proxies /api/* to Go backend
└── Static files in web/dist/ (production) → served by Go binary
Go Server (silod)
├── /api/* JSON REST API
├── /login, /logout Session auth endpoints (form POST)
├── /auth/oidc OIDC redirect flow
└── /* React SPA (NotFound handler serves index.html for client-side routing)
```
### Auth Flow
1. React app loads, `AuthProvider` calls `GET /api/auth/me`
2. If 401 → render `LoginPage` (React form)
3. Login form POSTs `application/x-www-form-urlencoded` to `/login` (Go handler sets session cookie)
4. On success, `AuthProvider.refresh()` re-fetches `/api/auth/me`, user state populates, app renders
5. OIDC: link to `/auth/oidc` triggers Go-served redirect flow, callback sets session, user returns to app
6. API client auto-redirects to `/login` on any 401 response
### Public API Endpoint
`GET /api/auth/config` — returns `{ oidc_enabled: bool, local_enabled: bool }` so the login page can conditionally show the OIDC button without hardcoding.
## File Structure
```
web/
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── src/
├── main.tsx Entry point, renders AuthProvider + BrowserRouter + App
├── App.tsx Route definitions, auth guard
├── api/
│ ├── client.ts fetch wrapper: get, post, put, del + ApiError class
│ └── types.ts All TypeScript interfaces (272 lines)
├── context/
│ └── AuthContext.tsx AuthProvider with login/logout/refresh methods
├── hooks/
│ ├── useAuth.ts Context consumer hook
│ ├── useFormDescriptor.ts Fetches form descriptor from /api/schemas/{name}/form (replaces useCategories)
│ ├── useItems.ts Items fetching with search, filters, pagination, debounce
│ └── useLocalStorage.ts Typed localStorage persistence hook
├── styles/
│ ├── theme.css Catppuccin Mocha CSS custom properties
│ └── global.css Base element styles
├── components/
│ ├── AppShell.tsx Header nav + user info + <Outlet/>
│ ├── ContextMenu.tsx Reusable right-click positioned menu
│ └── items/ Items page components (16 files)
│ ├── ItemsToolbar.tsx Search, filters, layout toggle, action buttons
│ ├── ItemTable.tsx Sortable table, column config, compact rows
│ ├── ItemDetail.tsx 5-tab detail panel (Main, Properties, Revisions, BOM, Where Used)
│ ├── MainTab.tsx Metadata display, project tags editor, file info
│ ├── PropertiesTab.tsx Form/JSON dual-mode property editor
│ ├── RevisionsTab.tsx Revision list, compare diff, status, rollback
│ ├── BOMTab.tsx BOM table with inline CRUD, cost calculations
│ ├── WhereUsedTab.tsx Parent assemblies table
│ ├── SplitPanel.tsx Resizable horizontal/vertical layout container
│ ├── FooterStats.tsx Fixed bottom bar with item counts
│ ├── CreateItemPane.tsx In-pane create form with schema category properties
│ ├── EditItemPane.tsx In-pane edit form
│ ├── DeleteItemPane.tsx In-pane delete confirmation
│ └── ImportItemsPane.tsx CSV upload with dry-run/import flow
└── pages/
├── LoginPage.tsx Username/password form + OIDC button
├── ItemsPage.tsx Orchestrator: toolbar, split panel, table, detail/CRUD panes
├── ProjectsPage.tsx Project CRUD with sortable table, in-pane forms
├── SchemasPage.tsx Schema browser with collapsible segments, enum value CRUD
├── SettingsPage.tsx Account info, API token management
└── AuditPage.tsx Audit completeness (placeholder, expanded in Issue #5)
```
**Total**: ~40 source files, ~7,600 lines of TypeScript/TSX.
## Design System
### Theme
Catppuccin Mocha dark theme. All colors referenced via CSS custom properties:
| Token | Use |
|-------|-----|
| `--ctp-base` | Page background, input backgrounds |
| `--ctp-mantle` | Header background |
| `--ctp-surface0` | Card backgrounds, even table rows |
| `--ctp-surface1` | Borders, dividers, hover states |
| `--ctp-surface2` | Secondary button backgrounds |
| `--ctp-text` | Primary text |
| `--ctp-subtext0/1` | Secondary/muted text, labels |
| `--ctp-overlay0` | Placeholder text |
| `--ctp-mauve` | Brand accent, primary buttons, nav active |
| `--ctp-blue` | Editor role badge, edit headers |
| `--ctp-green` | Success banners, create headers |
| `--ctp-red` | Errors, delete actions, danger buttons |
| `--ctp-peach` | Part numbers, project codes, token prefixes |
| `--ctp-teal` | Viewer role badge |
| `--ctp-sapphire` | Links, collapsible toggles |
| `--ctp-crust` | Dark text on colored backgrounds |
### Typography
- Body: system font stack (Inter, -apple-system, etc.)
- Monospace: JetBrains Mono (part numbers, codes, tokens)
- Table cells: 0.85rem
- Labels: 0.85rem, weight 500
- Table headers: 0.8rem, uppercase, letter-spacing 0.05em
### Component Patterns
**Tables**: Inline styles, compact rows (28-32px), alternating `base`/`surface0` backgrounds, sortable headers with arrow indicators, right-click column config (Items page).
**Forms**: In-pane forms (Infor ERP-style) — not modal overlays. Create/Edit/Delete forms render in the detail pane area with a colored header bar (green=create, blue=edit, red=delete). Cancel returns to previous view.
**Cards**: `surface0` background, `0.75rem` border radius, `1.5rem` padding.
**Buttons**: Primary (`mauve` bg, `crust` text), secondary (`surface1` bg), danger (`red` bg or translucent red bg with red text).
**Errors**: Red text with translucent red background banner, `0.4rem` border radius.
**Role badges**: Colored pill badges — admin=mauve, editor=blue, viewer=teal.
## Page Specifications
### Items Page (completed in #8)
The most complex page. Master-detail layout with resizable split panel.
**Toolbar**: Debounced search (300ms) with scope toggle (All/PN/Description), type and project filter dropdowns, layout toggle (horizontal/vertical), export/import/create buttons.
**Table**: 7 configurable columns (part_number, item_type, description, revision, projects, created, actions). Visibility stored per layout mode in localStorage. Right-click header opens ContextMenu with checkboxes. Compact rows, zebra striping, click to select.
**Detail panel**: 5 tabs — Main (metadata + project tags + file info), Properties (form/JSON editor, save creates revision), Revisions (compare, status management, rollback), BOM (inline CRUD, cost calculations, CSV export), Where Used (parent assemblies).
**CRUD panes**: In-pane forms for Create (schema category properties, project tags), Edit (basic fields), Delete (confirmation), Import (CSV upload with dry-run).
**Footer**: Fixed 28px bottom bar showing Total | Parts | Assemblies | Documents counts, reactive to filters.
**State**: `PaneMode` discriminated union manages which pane is shown. `useItems` hook handles fetching, search, filters, pagination. `useLocalStorage` persists layout and column preferences.
### Projects Page (completed in #9)
Sortable table with columns: Code, Name, Description, Items (count fetched per project), Created, Actions.
**CRUD**: In-pane forms above the table. Create requires code (2-10 chars, auto-uppercase), name, description. Edit allows name and description changes. Delete shows confirmation with project code.
**Navigation**: Click project code navigates to Items page with `?project=CODE` filter.
**Permissions**: Create/Edit/Delete buttons only visible to editor/admin roles.
### Schemas Page (completed in #9)
Schema cards with collapsible segment details. Each schema shows name, description, format string, version, and example part numbers.
**Segments**: Expandable list showing segment name, type badge, description. Enum segments include a values table with code and description columns.
**Enum CRUD**: Inline table operations — add row at bottom, edit replaces the row, delete highlights the row with confirmation. All operations call `POST/PUT/DELETE /api/schemas/{name}/segments/{segment}/values/{code}`.
### Settings Page (completed in #9)
Two cards:
**Account**: Read-only grid showing username, display name, email, auth source, role (with colored badge). Data from `useAuth()` context.
**API Tokens**: Create form (name input + button), one-time token display in green banner with copy-to-clipboard, token list table (name, prefix, created, last used, expires, revoke). Revoke has inline confirm step. Uses `GET/POST/DELETE /api/auth/tokens`.
### Login Page (completed in #9)
Standalone centered card (no AppShell). Username/password form, OIDC button shown conditionally based on `GET /api/auth/config`. Error messages in red banner. Submit calls `AuthContext.login()` which POSTs form data to `/login` then re-fetches the user.
### Audit Page (placeholder)
Basic table showing audit completeness data from `GET /api/audit/completeness`. Will be expanded as part of Issue #5 (Component Audit UI with completeness scoring and inline editing).
## API Client
`web/src/api/client.ts` — thin wrapper around `fetch`:
- Always sends `credentials: 'include'` for session cookies
- Always sets `Content-Type: application/json`
- 401 responses redirect to `/login`
- Non-OK responses parsed as `{ error, message }` and thrown as `ApiError`
- 204 responses return `undefined`
- Exports: `get<T>()`, `post<T>()`, `put<T>()`, `del()`
## Type Definitions
`web/src/api/types.ts` — 272 lines covering all API response and request shapes:
**Core models**: User, Item, Project, Schema, SchemaSegment, Revision, BOMEntry
**Audit**: AuditFieldResult, AuditItemResult, AuditSummary, AuditCompletenessResponse
**Search**: FuzzyResult (extends Item with score)
**BOM**: WhereUsedEntry, AddBOMEntryRequest, UpdateBOMEntryRequest
**Items**: CreateItemRequest, UpdateItemRequest, CreateRevisionRequest
**Projects**: CreateProjectRequest, UpdateProjectRequest
**Schemas**: CreateSchemaValueRequest, UpdateSchemaValueRequest, PropertyDef, PropertySchema
**Auth**: AuthConfig, ApiToken, ApiTokenCreated
**Revisions**: RevisionComparison
**Import**: CSVImportResult, CSVImportError
**Errors**: ErrorResponse
## Completed Work
### Issue #10: Remove Go Templates + Docker Integration -- COMPLETE
Completed in commit `50923cf`. All Go templates deleted, `web.go` handler removed, SPA serves at `/` via `NotFound` handler with `index.html` fallback. `build/package/Dockerfile` added.
### Remaining Work
### Issue #5: Component Audit UI (future)
The Audit page will be expanded with completeness scoring, inline editing, tier filtering, and category breakdowns. This will be built natively in React using the patterns established in the migration.
## Development
```bash
# Install dependencies
cd web && npm install
# Dev server (proxies /api/* to Go backend on :8080)
npm run dev
# Type check
npx tsc --noEmit
# Production build
npm run build
```
Vite dev server runs on port 5173 with proxy config in `vite.config.ts` forwarding `/api/*`, `/login`, `/logout`, `/auth/*` to the Go backend.
## Conventions
- **No modals for CRUD** — use in-pane forms (Infor ERP-style pattern)
- **No shared component library extraction** until a pattern repeats 3+ times
- **Inline styles only** — all styling via `React.CSSProperties` objects, using Catppuccin CSS variables
- **No class components** — functional components with hooks only
- **Permission checks**: derive `isEditor` from `user.role` in each page, conditionally render write actions
- **Error handling**: try/catch with error state, display in red banners inline
- **Data fetching**: `useEffect` + API client on mount, loading/error/data states
- **Persistence**: `useLocalStorage` hook for user preferences (layout mode, column visibility)
## New Frontend Tasks
# CreateItemPane — Schema-Driven Dynamic Form
**Date**: 2026-02-10
**Scope**: `CreateItemPane.tsx` renders a dynamic form driven entirely by the form descriptor API (`GET /api/schemas/{name}/form`). All field groups, field types, widgets, and category-specific fields are defined in YAML and resolved server-side.
**Parent**: Items page (`ItemsPage.tsx`) — renders in the detail pane area per existing in-pane CRUD pattern.
---
## Layout
Single-column scrollable form with a green header bar. Field groups are rendered dynamically from the form descriptor. Category-specific field groups appear after global groups when a category is selected.
```
┌──────────────────────────────────────────────────────────────────────┐
│ Header: "New Item" [green bar] Cancel │ Create │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Category * [Domain buttons: F C R S E M T A P X] │
│ [Subcategory search + filtered list] │
│ │
│ ── Identity ────────────────────────────────────────────────────── │
│ [Type * (auto-derived from category)] [Description ] │
│ │
│ ── Sourcing ────────────────────────────────────────────────────── │
│ [Sourcing Type v] [Manufacturer] [MPN] [Supplier] [SPN] │
│ [Sourcing Link] │
│ │
│ ── Cost & Lead Time ────────────────────────────────────────────── │
│ [Standard Cost $] [Lead Time Days] [Min Order Qty] │
│ │
│ ── Status ──────────────────────────────────────────────────────── │
│ [Lifecycle Status v] [RoHS Compliant ☐] [Country of Origin] │
│ │
│ ── Details ─────────────────────────────────────────────────────── │
│ [Long Description ] │
│ [Projects: [tag][tag] type to search... ] │
│ [Notes ] │
│ │
│ ── Fastener Specifications (category-specific) ─────────────────── │
│ [Material] [Finish] [Thread Size] [Head Type] [Drive Type] ... │
│ │
└──────────────────────────────────────────────────────────────────────┘
```
## Data Source — Form Descriptor API
All form structure is fetched from `GET /api/schemas/kindred-rd/form`, which returns:
- `category_picker`: Multi-stage picker config (domain → subcategory)
- `item_fields`: Definitions for item-level fields (description, item_type, sourcing_type, etc.)
- `field_groups`: Ordered groups with resolved field metadata (Identity, Sourcing, Cost, Status, Details)
- `category_field_groups`: Per-category-prefix groups (e.g., Fastener Specifications for `F` prefix)
- `field_overrides`: Widget hints (currency, url, select, checkbox)
The YAML schema (`schemas/kindred-rd.yaml`) is the single source of truth. Adding a new field or category in YAML propagates to all clients with no code changes.
## File Location
`web/src/components/items/CreateItemPane.tsx`
Supporting files:
| File | Purpose |
|------|---------|
| `web/src/components/items/CategoryPicker.tsx` | Multi-stage domain/subcategory selector |
| `web/src/components/items/FileDropZone.tsx` | Drag-and-drop file upload with MinIO presigned URLs |
| `web/src/components/items/TagInput.tsx` | Multi-select tag input for projects |
| `web/src/hooks/useFormDescriptor.ts` | Fetches and caches form descriptor from `/api/schemas/{name}/form` |
| `web/src/hooks/useFileUpload.ts` | Manages presigned URL upload flow |
## Component Breakdown
### CreateItemPane
Top-level orchestrator. Renders dynamic form from the form descriptor.
**Props** (unchanged interface):
```typescript
interface CreateItemPaneProps {
onCreated: (item: Item) => void;
onCancel: () => void;
}
```
**State**:
```typescript
const { descriptor, categories, loading } = useFormDescriptor();
const [category, setCategory] = useState(''); // selected category code, e.g. "F01"
const [fields, setFields] = useState<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 (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)`.
**Header bar**: Green (`--ctp-green` background, `--ctp-crust` text). "New Item" title on left, Cancel and Create Item buttons on right.
### CategoryPicker
Multi-stage category selector driven by the form descriptor's `category_picker.stages` config.
**Props**:
```typescript
interface CategoryPickerProps {
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**: Two-stage selection:
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.
If no `stages` prop is provided, falls back to a flat searchable list of all categories.
Below the picker, the selected category is shown as a breadcrumb: `Fasteners F01 — Hex Cap Screw` in `--ctp-mauve`.
**Data source**: Categories come from `useFormDescriptor()` which derives them from the `category_picker` stages and `values_by_domain` in the form descriptor response.
### FileDropZone
Handles drag-and-drop and click-to-browse file uploads with MinIO presigned URL flow.
**Props**:
```typescript
interface FileDropZoneProps {
files: PendingAttachment[];
onFilesAdded: (files: PendingAttachment[]) => void;
onFileRemoved: (index: number) => void;
accept?: string; // e.g. '.FCStd,.step,.stl,.pdf,.png,.jpg'
}
interface PendingAttachment {
file: File;
objectKey: string; // MinIO key after upload
uploadProgress: number; // 0-100
uploadStatus: 'pending' | 'uploading' | 'complete' | 'error';
error?: string;
}
```
**Drop zone UI**: Dashed `2px` border using `--ctp-surface1`, `border-radius: 0.5rem`, centered content with a paperclip icon (Unicode 📎 or inline SVG), "Drop files here or **browse**" text, and accepted formats in `--ctp-overlay0` at 10px.
States:
- **Default**: dashed border `--ctp-surface1`
- **Drag over**: dashed border `--ctp-mauve`, background `rgba(203, 166, 247, 0.05)`
- **Uploading**: show progress per file in the file list
Clicking the zone opens a hidden `<input type="file" multiple>`.
**File list**: Rendered below the drop zone. Each file shows:
- Type icon: colored 28×28 rounded square. Color mapping: `.FCStd`/`.step`/`.stl``--ctp-blue` ("CAD"), `.pdf``--ctp-red` ("PDF"), `.png`/`.jpg``--ctp-green` ("IMG"), other → `--ctp-overlay1` ("FILE").
- File name (truncated with ellipsis).
- File size + type label in `--ctp-overlay0` at 10px.
- Upload progress bar (thin 2px bar under the file item, `--ctp-mauve` fill) when uploading.
- Remove button (`×`) on the right, `--ctp-overlay0``--ctp-red` on hover.
**Upload flow** (managed by `useFileUpload` hook):
1. On file selection/drop, immediately request a presigned upload URL: `POST /api/uploads/presign` with `{ filename, content_type, size }`.
2. Backend returns `{ object_key, upload_url, expires_at }`.
3. `PUT` the file directly to the presigned MinIO URL using `XMLHttpRequest` (for progress tracking).
4. On completion, update `PendingAttachment.uploadStatus` to `'complete'` and store the `object_key`.
5. The `object_key` is later sent to the item creation endpoint to associate the file.
If the presigned URL endpoint doesn't exist yet, see Backend Changes.
### TagInput
Reusable multi-select input for projects (and potentially other tag-like fields).
**Props**:
```typescript
interface TagInputProps {
value: string[]; // selected project IDs
onChange: (ids: string[]) => void;
placeholder?: string;
searchFn: (query: string) => Promise<{ id: string; label: string }[]>;
}
```
**Rendering**: Container styled like a form input (`--ctp-crust` bg, `--ctp-surface1` border, `border-radius: 0.4rem`). Inside:
- Selected tags as inline pills: `rgba(203, 166, 247, 0.15)` bg, `--ctp-mauve` text, 11px font, with `×` remove button.
- A bare `<input>` (no border/bg) that grows to fill remaining width, `min-width: 80px`.
**Behavior**: On typing, debounce 200ms, call `searchFn(query)`. Show a dropdown below the input with matching results. Click or Enter selects. Already-selected items are excluded from results. Escape or blur closes the dropdown.
The dropdown is an absolutely-positioned `<div>` below the input container, `--ctp-crust` background, `--ctp-surface1` border, `border-radius: 0.4rem`, `max-height: 160px`, `overflow-y: auto`. Each row is 28px, hover `--ctp-surface0`.
**For projects**: `searchFn` calls `GET /api/projects?q={query}` and maps to `{ id: project.id, label: project.code + ' — ' + project.name }`.
### useFormDescriptor Hook
```typescript
function useFormDescriptor(schemaName = "kindred-rd"): {
descriptor: FormDescriptor | null;
categories: Record<string, string>; // flat code → description map derived from descriptor
loading: boolean;
}
```
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
```typescript
function useFileUpload(): {
upload: (file: File) => Promise<PendingAttachment>;
uploading: boolean;
}
```
Encapsulates the presigned URL flow. Returns a function that takes a `File`, gets a presigned URL, uploads via XHR with progress tracking, and returns the completed `PendingAttachment`. The component manages the array of attachments in its own state.
## Styling
All styling via inline `React.CSSProperties` objects, per project convention. Reference Catppuccin tokens through `var(--ctp-*)` strings. No CSS modules, no Tailwind, no class names.
Common style patterns to extract as `const` objects at the top of each file:
```typescript
const styles = {
container: {
display: 'grid',
gridTemplateColumns: '1fr 320px',
height: '100%',
overflow: 'hidden',
} as React.CSSProperties,
formArea: {
padding: '1.5rem 2rem',
overflowY: 'auto',
} as React.CSSProperties,
formGrid: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1.25rem 1.5rem',
maxWidth: '800px',
} as React.CSSProperties,
sidebar: {
background: 'var(--ctp-mantle)',
borderLeft: '1px solid var(--ctp-surface0)',
display: 'flex',
flexDirection: 'column' as const,
overflowY: 'auto',
} as React.CSSProperties,
// ... etc
};
```
## Form Sections
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`).
**Global field groups** (from `ui.field_groups` in YAML):
| 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 |
**Category-specific field groups** (from `ui.category_field_groups` in YAML, shown when a category is selected):
| 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) |
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-5 below are implemented. Item 4 (hierarchical categories) is resolved by the form descriptor's multi-stage category picker.
### 1. Presigned Upload URL -- IMPLEMENTED
```
POST /api/uploads/presign
Request: { "filename": "bracket.FCStd", "content_type": "application/octet-stream", "size": 2400000 }
Response: { "object_key": "uploads/tmp/{uuid}/{filename}", "upload_url": "https://minio.../...", "expires_at": "2026-02-06T..." }
```
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 -- IMPLEMENTED
```
POST /api/items/{id}/files
Request: { "object_key": "uploads/tmp/{uuid}/bracket.FCStd", "filename": "bracket.FCStd", "content_type": "...", "size": 2400000 }
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 -- IMPLEMENTED
```
PUT /api/items/{id}/thumbnail
Request: { "object_key": "uploads/tmp/{uuid}/thumb.png" }
Response: 204
```
Stores the thumbnail at `items/{item_id}/thumbnail.png` in MinIO. Updates `item.thumbnail_key` column.
### 4. Hierarchical Categories -- IMPLEMENTED (via Form Descriptor)
Resolved by the schema-driven form descriptor (`GET /api/schemas/{name}/form`). The YAML schema's `ui.category_picker` section defines multi-stage selection:
- **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.
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
```sql
CREATE TABLE item_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size BIGINT NOT NULL DEFAULT 0,
object_key TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_item_files_item ON item_files(item_id);
ALTER TABLE items ADD COLUMN thumbnail_key TEXT;
ALTER TABLE items ADD COLUMN sourcing_type TEXT NOT NULL DEFAULT 'manufactured';
ALTER TABLE items ADD COLUMN unit_of_measure TEXT NOT NULL DEFAULT 'ea';
ALTER TABLE items ADD COLUMN long_description TEXT;
```
## Implementation Order
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).
## Types Added
The following types were added to `web/src/api/types.ts` for the form descriptor system:
```typescript
// Form descriptor types (from GET /api/schemas/{name}/form)
interface FormFieldDescriptor {
name: string;
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 FormFieldGroup {
key: string;
label: string;
order: number;
fields: FormFieldDescriptor[];
}
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;
size: number;
}
interface PresignResponse {
object_key: string;
upload_url: string;
expires_at: string;
}
interface ItemFile {
id: string;
item_id: string;
filename: string;
content_type: string;
size: number;
object_key: string;
created_at: string;
}
// Pending upload (frontend only, not an API type)
interface PendingAttachment {
file: File;
objectKey: string;
uploadProgress: number;
uploadStatus: 'pending' | 'uploading' | 'complete' | 'error';
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

@@ -0,0 +1,128 @@
# Kindred Silo
Item database and part management system.
## Overview
Kindred Silo is an R&D-oriented item database with:
- **Configurable part number generation** via YAML schemas
- **Revision tracking** with append-only history, rollback, comparison, and status labels
- **BOM management** with multi-level expansion, flat BOM flattening, assembly costing, where-used queries, CSV/ODS export
- **Authentication** with local (bcrypt), LDAP/FreeIPA, and OIDC/Keycloak backends
- **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** — 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)
## Components
```
silo/
├── cmd/
│ ├── silo/ # CLI tool
│ └── silod/ # API server
├── internal/
│ ├── api/ # HTTP handlers and routes (78 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
See the **[Installation Guide](docs/INSTALL.md)** for complete setup instructions.
**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`.
```bash
# CLI usage
go run ./cmd/silo register --schema kindred-rd --category F01
```
## Configuration
See `config.example.yaml` for all options.
## Authentication
Silo supports three authentication backends, configured in `config.yaml`:
| Backend | Description |
|---------|-------------|
| **Local** | Built-in accounts with bcrypt passwords |
| **LDAP** | FreeIPA / Active Directory integration |
| **OIDC** | Keycloak / OpenID Connect providers |
Roles: **admin** (full access) > **editor** (create/modify items) > **viewer** (read-only).
API tokens provide programmatic access for scripts and CAD clients. Set `auth.enabled: false` for development without authentication.
See [docs/AUTH.md](docs/AUTH.md) for full details.
## Client Integrations
CAD and spreadsheet integrations are maintained in separate repositories:
- **Kindred Create / FreeCAD workbench** -- [silo-mod](https://git.kindred-systems.com/kindred/silo-mod)
- **LibreOffice Calc extension** -- [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)
The server provides the REST API and ODS endpoints consumed by these clients.
## Documentation
| 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 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 |
| [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
MIT License - Copyright (c) 2026 Kindred Systems LLC
See [LICENSE](LICENSE) for details.

8
docs/theme/kindred.css vendored Normal file
View File

@@ -0,0 +1,8 @@
/* Kindred Create docs - minor overrides for coal theme */
:root {
--sidebar-width: 280px;
}
.sidebar .sidebar-scrollbox {
padding: 10px 15px;
}

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="6" y="6" width="9" height="9" rx="1" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<rect x="17" y="6" width="9" height="9" rx="1" fill="none" stroke="#a6e3a1" stroke-width="1.5"/>
<rect x="6" y="17" width="9" height="9" rx="1" fill="none" stroke="#f9e2af" stroke-width="1.5"/>
<rect x="17" y="17" width="9" height="9" rx="1" fill="none" stroke="#cba6f7" stroke-width="1.5"/>
<line x1="21" y1="19" x2="21" y2="25" stroke="#cba6f7" stroke-width="1.5"/>
<line x1="18" y1="22" x2="24" y2="22" stroke="#cba6f7" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 675 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">3V</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="8" font-weight="bold" fill="#fab387" opacity="0.8">Add</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Axis</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">AS</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="8" font-weight="bold" fill="#fab387" opacity="0.8">AST</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">AT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Bims</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Buil</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">BP</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="8" font-weight="bold" fill="#fab387" opacity="0.8">BPT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">BT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Cell</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Chec</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CH</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Comp</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CC</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CW</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="8" font-weight="bold" fill="#fab387" opacity="0.8">CWT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">CP</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Equi</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">EC</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">ET</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Fenc</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">FT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Fixt</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Floo</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">FT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Fram</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">FT</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Grid</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="7" font-weight="bold" fill="#fab387" opacity="0.8">Mate</text>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">MG</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<rect x="5" y="5" width="22" height="22" rx="3" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="3,2" opacity="0.6"/>
<text x="16" y="17" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="10" font-weight="bold" fill="#fab387" opacity="0.8">MM</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

Some files were not shown because too many files have changed in this diff Show More