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
82
.gitea/ISSUE_TEMPLATE/bug_report.yml
Normal 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
|
||||
42
.gitea/ISSUE_TEMPLATE/documentation.yml
Normal 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
|
||||
55
.gitea/ISSUE_TEMPLATE/feature_request.yml
Normal 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
|
||||
18
.gitea/pull_request_template.md
Normal 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)
|
||||
11
.gitea/runner/cleanup.service
Normal 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
@@ -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
|
||||
10
.gitea/runner/cleanup.timer
Normal 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
@@ -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
@@ -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/
|
||||
438
.gitea/workflows/release.yml
Normal 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
|
||||
53
.gitea/workflows/sync-silo-docs.yml
Normal 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
@@ -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
|
||||
|
||||
200
CONTRIBUTING.md
@@ -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
|
After Width: | Height: | Size: 18 KiB |
72
docs/ARCHITECTURE.md
Normal 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
@@ -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
@@ -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
84
docs/KNOWN_ISSUES.md
Normal 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
@@ -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
@@ -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
@@ -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)
|
||||
27
docs/src/architecture/ondsel-solver.md
Normal 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.
|
||||
78
docs/src/architecture/overview.md
Normal 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
|
||||
26
docs/src/architecture/python-source-of-truth.md
Normal 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)
|
||||
274
docs/src/architecture/signal-architecture.md
Normal 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
|
||||
11
docs/src/architecture/silo-server.md
Normal 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
|
||||
72
docs/src/development/build-system.md
Normal 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
|
||||
40
docs/src/development/code-quality.md
Normal 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.
|
||||
52
docs/src/development/contributing.md
Normal 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).
|
||||
239
docs/src/development/gui-build-integration.md
Normal 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
|
||||
73
docs/src/development/repo-structure.md
Normal 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
@@ -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.
|
||||
41
docs/src/guide/getting-started.md
Normal 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.
|
||||
49
docs/src/guide/installation.md
Normal 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
@@ -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)
|
||||
23
docs/src/guide/workbenches.md
Normal 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
@@ -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.0–1.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.0–1.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 (Q1–Q4 2026)
|
||||
37
docs/src/introduction.md
Normal 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>
|
||||
110
docs/src/reference/configuration.md
Normal 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.
|
||||
142
docs/src/reference/cpp-command-origin.md
Normal 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
|
||||
537
docs/src/reference/cpp-custom-origin-guide.md
Normal 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
|
||||
257
docs/src/reference/cpp-file-origin-python.md
Normal 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
|
||||
199
docs/src/reference/cpp-file-origin.md
Normal 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)
|
||||
218
docs/src/reference/cpp-local-file-origin.md
Normal 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
|
||||
226
docs/src/reference/cpp-origin-manager.md
Normal 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
|
||||
224
docs/src/reference/cpp-origin-selector-widget.md
Normal 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
|
||||
53
docs/src/reference/glossary.md
Normal 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> |
|
||||
226
docs/src/silo-server/AUTH.md
Normal 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
|
||||
185
docs/src/silo-server/AUTH_MIDDLEWARE.md
Normal 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
|
||||
```
|
||||
257
docs/src/silo-server/AUTH_USER_GUIDE.md
Normal 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`
|
||||
0
docs/src/silo-server/BOM_MERGE.md
Normal file
16
docs/src/silo-server/CALC_EXTENSION.md
Normal 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 |
|
||||
523
docs/src/silo-server/COMPONENT_AUDIT.md
Normal 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.
|
||||
364
docs/src/silo-server/CONFIGURATION.md
Normal 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"
|
||||
```
|
||||
470
docs/src/silo-server/DEPLOYMENT.md
Normal 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)
|
||||
452
docs/src/silo-server/GAP_ANALYSIS.md
Normal 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
|
||||
```
|
||||
518
docs/src/silo-server/INSTALL.md
Normal 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 |
|
||||
536
docs/src/silo-server/ROADMAP.md
Normal 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 |
|
||||
948
docs/src/silo-server/SPECIFICATION.md
Normal 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
|
||||
```
|
||||
98
docs/src/silo-server/STATUS.md
Normal 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 |
|
||||
757
docs/src/silo-server/frontend-spec.md
Normal 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.
|
||||
128
docs/src/silo-server/overview.md
Normal 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
@@ -0,0 +1,8 @@
|
||||
/* Kindred Create docs - minor overrides for coal theme */
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
}
|
||||
|
||||
.sidebar .sidebar-scrollbox {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
9
kindred-icons/AddonManager.svg
Normal 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 |
5
kindred-icons/Arch_3Views.svg
Normal 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 |
5
kindred-icons/Arch_Add.svg
Normal 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 |
5
kindred-icons/Arch_Axis.svg
Normal 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 |
5
kindred-icons/Arch_Axis_System.svg
Normal 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 |
5
kindred-icons/Arch_Axis_System_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Axis_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Bimserver.svg
Normal 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 |
5
kindred-icons/Arch_Building.svg
Normal 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 |
5
kindred-icons/Arch_BuildingPart.svg
Normal 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 |
5
kindred-icons/Arch_BuildingPart_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Building_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Cell.svg
Normal 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 |
5
kindred-icons/Arch_Cell_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Check.svg
Normal 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 |
5
kindred-icons/Arch_CloseHoles.svg
Normal 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 |
5
kindred-icons/Arch_Component.svg
Normal 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 |
5
kindred-icons/Arch_Component_Clone.svg
Normal 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 |
5
kindred-icons/Arch_Component_Tree.svg
Normal 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 |
5
kindred-icons/Arch_CurtainWall.svg
Normal 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 |
5
kindred-icons/Arch_CurtainWall_Tree.svg
Normal 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 |
5
kindred-icons/Arch_CutPlane.svg
Normal 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 |
5
kindred-icons/Arch_Equipment.svg
Normal 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 |
5
kindred-icons/Arch_Equipment_Clone.svg
Normal 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 |
5
kindred-icons/Arch_Equipment_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Fence.svg
Normal 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 |
5
kindred-icons/Arch_Fence_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Fixture.svg
Normal 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 |
5
kindred-icons/Arch_Floor.svg
Normal 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 |
5
kindred-icons/Arch_Floor_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Frame.svg
Normal 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 |
5
kindred-icons/Arch_Frame_Tree.svg
Normal 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 |
5
kindred-icons/Arch_Grid.svg
Normal 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 |
5
kindred-icons/Arch_Material.svg
Normal 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 |
5
kindred-icons/Arch_Material_Group.svg
Normal 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 |
5
kindred-icons/Arch_Material_Multi.svg
Normal 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 |