Merge pull request 'docs: update server docs' (#377) from fix/rattler-build-submodules into main
Some checks are pending
Deploy Docs / build-and-deploy (push) Successful in 36s
Build and Test / build (push) Has started running

Reviewed-on: #377
This commit was merged in pull request #377.
This commit is contained in:
2026-03-03 19:36:39 +00:00
17 changed files with 512 additions and 1953 deletions

View File

@@ -1,132 +0,0 @@
# KCSolve: Pluggable Solver Architecture
KCSolve is the pluggable assembly constraint solver framework for Kindred Create. It defines an abstract solver interface (`IKCSolver`) and a runtime registry (`SolverRegistry`) that lets the Assembly module work with any conforming solver backend. The default backend wraps OndselSolver via `OndselAdapter`.
- **Library:** `src/Mod/Assembly/Solver/` (builds `libKCSolve.so`)
- **Python module:** `src/Mod/Assembly/Solver/bindings/` (builds `kcsolve.so`)
- **Tests:** `tests/src/Mod/Assembly/Solver/` (C++), `src/Mod/Assembly/AssemblyTests/TestKCSolvePy.py` (Python)
## Architecture
```
┌──────────────────────────────────────────────────┐
│ Assembly Module (AssemblyObject.cpp) │
│ Builds SolveContext from FreeCAD document, │
│ calls solver via SolverRegistry │
├──────────────────────────────────────────────────┤
│ SolverRegistry (singleton) │
│ register_solver(), get(), available() │
│ Plugin discovery via scan() / scan_default_paths │
├──────────────┬───────────────────────────────────┤
│ OndselAdapter │ Python solvers │ Future plugins │
│ (C++ built-in)│ (via kcsolve) │ (.so plugins) │
└──────────────┴───────────────────────────────────┘
```
The Assembly module never references OndselSolver directly. All solver access goes through `SolverRegistry::instance().get()`, which returns a `std::unique_ptr<IKCSolver>`.
## IKCSolver interface
A solver backend implements `IKCSolver` (defined in `IKCSolver.h`). Only three methods are pure virtual; all others have sensible defaults:
| Method | Required | Purpose |
|--------|----------|---------|
| `name()` | yes | Human-readable solver name |
| `supported_joints()` | yes | List of `BaseJointKind` values the solver handles |
| `solve(ctx)` | yes | Solve for static equilibrium |
| `update(ctx)` | no | Incremental re-solve after parameter changes |
| `pre_drag(ctx, parts)` | no | Begin interactive drag session |
| `drag_step(placements)` | no | One mouse-move during drag |
| `post_drag()` | no | End drag session |
| `run_kinematic(ctx)` | no | Run kinematic simulation |
| `num_frames()` | no | Frame count after simulation |
| `update_for_frame(i)` | no | Retrieve frame placements |
| `diagnose(ctx)` | no | Detect redundant/conflicting constraints |
| `is_deterministic()` | no | Whether output is reproducible (default: true) |
| `export_native(path)` | no | Write solver-native debug file (e.g. ASMT) |
| `supports_bundle_fixed()` | no | Whether solver handles Fixed-joint bundling internally |
## Core types
All types live in `Types.h` with no FreeCAD dependencies, making the header standalone for future server/worker use.
**Transform** -- position `[x, y, z]` + unit quaternion `[w, x, y, z]`. Equivalent to `Base::Placement` but independent. Note the quaternion convention differs from `Base::Rotation` which uses `(x, y, z, w)` ordering; the adapter layer handles the swap.
**BaseJointKind** -- 24 primitive constraint types decomposed from FreeCAD's `JointType` and `DistanceType` enums. Covers point constraints (Coincident, PointOnLine, PointInPlane), axis/surface constraints (Concentric, Tangent, Planar), kinematic joints (Fixed, Revolute, Cylindrical, Slider, Ball, Screw, Universal), mechanical elements (Gear, RackPinion), distance variants, and a `Custom` extension point.
**SolveContext** -- complete solver input: parts (with placements, mass, grounded flag), constraints (with markers, parameters, limits), optional motion definitions and simulation parameters.
**SolveResult** -- solver output: status code, updated part placements, DOF count, constraint diagnostics, and simulation frame count.
## SolverRegistry
Thread-safe singleton managing solver backends:
```cpp
auto& reg = SolverRegistry::instance();
// Registration (at module init)
reg.register_solver("ondsel", []() {
return std::make_unique<OndselAdapter>();
});
// Retrieval
auto solver = reg.get(); // default solver
auto solver = reg.get("ondsel"); // by name
// Queries
reg.available(); // ["ondsel", ...]
reg.joints_for("ondsel"); // [Fixed, Revolute, ...]
reg.set_default("ondsel");
```
Plugin discovery scans directories for shared libraries exporting `kcsolve_api_version()` and `kcsolve_create()`. Default paths: `KCSOLVE_PLUGIN_PATH` env var and `<prefix>/lib/kcsolve/`.
## OndselAdapter
The built-in solver backend wrapping OndselSolver's Lagrangian constraint formulation. Registered as `"ondsel"` at Assembly module initialization.
Supports all 24 joint types. The adapter translates between `SolveContext`/`SolveResult` and OndselSolver's internal `ASMTAssembly` representation, including:
- Part placement conversion (Transform <-> Base::Placement quaternion ordering)
- Constraint parameter mapping (BaseJointKind -> OndselSolver joint classes)
- Interactive drag protocol (pre_drag/drag_step/post_drag)
- Kinematic simulation (run_kinematic/num_frames/update_for_frame)
- Constraint diagnostics (redundancy detection via MbD system)
## Python bindings (kcsolve module)
The `kcsolve` pybind11 module exposes the full C++ API to Python. See [KCSolve Python API](../reference/kcsolve-python.md) for details.
Key capabilities:
- All enums, structs, and classes accessible from Python
- Subclass `IKCSolver` in pure Python to create new solver backends
- Register Python solvers at runtime via `kcsolve.register_solver()`
- Query the registry from the FreeCAD console
## File layout
```
src/Mod/Assembly/Solver/
├── Types.h # Enums and structs (no FreeCAD deps)
├── IKCSolver.h # Abstract solver interface
├── SolverRegistry.h/cpp # Singleton registry + plugin loading
├── OndselAdapter.h/cpp # OndselSolver wrapper
├── KCSolveGlobal.h # DLL export macros
├── CMakeLists.txt # Builds libKCSolve.so
└── bindings/
├── PyIKCSolver.h # pybind11 trampoline for Python subclasses
├── kcsolve_py.cpp # Module definition (enums, structs, classes)
└── CMakeLists.txt # Builds kcsolve.so (pybind11 module)
```
## Testing
- **18 C++ tests** (`KCSolve_tests_run`) covering SolverRegistry (8 tests) and OndselAdapter (10 tests including drag protocol and redundancy diagnosis)
- **16 Python tests** (`TestKCSolvePy`) covering module import, type bindings, registry functions, Python solver subclassing, and full register/load/solve round-trips
- **6 Python integration tests** (`TestSolverIntegration`) testing solver behavior through FreeCAD document objects
## Related
- [KCSolve Python API Reference](../reference/kcsolve-python.md)
- [INTER_SOLVER.md](../../INTER_SOLVER.md) -- full architecture specification

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,441 +0,0 @@
# QuickNav — Keyboard Navigation Addon Specification
**Addon name:** QuickNav
**Type:** Pure Python FreeCAD addon (no C++ required)
**Compatibility:** FreeCAD 1.0+, Kindred Create 0.1+
**Location:** `mods/quicknav/`
---
## 1. Overview
QuickNav provides keyboard-driven command access for FreeCAD and Kindred Create. It replaces mouse-heavy toolbar navigation with a numbered key system organized by workbench and command grouping. The addon is activated by loading its workbench and toggled on/off with the `0` key.
### Design Goals
- Numbers `1-9` execute commands within the active command grouping
- `Shift+1-9` switches command grouping within the active workbench
- `Ctrl+1-9` switches workbench context
- All groupings and workbenches are ordered by most-recently-used (MRU) history
- History is unlimited internally, top 9 shown, remainder scrollable/clickable
- Mouse interaction remains fully functional — QuickNav is purely additive
- Configuration persisted via `FreeCAD.ParamGet()`
---
## 2. Terminology
| Term | Definition |
|------|-----------|
| **Workbench** | A FreeCAD workbench (Sketcher, PartDesign, Assembly, etc.). Fixed assignment to Ctrl+N slots. |
| **Command Grouping** | A logical group of commands within a workbench, mapped from existing FreeCAD toolbar groupings. Max 9 per tier. |
| **Active Grouping** | The left-most visible grouping in the navigation bar. Its commands are accessible via `1-9`. |
| **Navigation Bar** | Bottom toolbar displaying the current state: active workbench, groupings, and numbered commands. |
| **MRU Stack** | Most-recently-used ordering. Position 0 = currently active, 1 = previously active, etc. |
| **Tier** | When a workbench has >9 command groupings, they are split: Tier 1 (most common 9), Tier 2 (next 9). |
---
## 3. Key Bindings
### 3.1 Mode Toggle
| Key | Action |
|-----|--------|
| `0` | Toggle QuickNav on/off. When off, all QuickNav key interception is disabled and the navigation bar hides. |
### 3.2 Command Execution
| Key | Action |
|-----|--------|
| `1-9` | Execute the Nth command in the active grouping. If the command is auto-executable (e.g., Pad after closed sketch), execute immediately. Otherwise, enter tool mode (same as clicking the toolbar button). |
### 3.3 Grouping Navigation
| Key | Action |
|-----|--------|
| `Shift+1-9` | Switch to the Nth command grouping (MRU ordered) within the current workbench. The newly activated grouping moves to position 0 in the MRU stack. |
| `Shift+Left/Right` | Scroll through groupings beyond the visible 9. |
### 3.4 Workbench Navigation
| Key | Action |
|-----|--------|
| `Ctrl+1` | Sketcher |
| `Ctrl+2` | Part Design |
| `Ctrl+3` | Assembly |
| `Ctrl+4` | Spreadsheet |
| `Ctrl+5` | TechDraw |
| `Ctrl+6-9` | User-configurable / additional workbenches |
Switching workbench via `Ctrl+N` also restores that workbench's last-active command grouping.
---
## 4. Navigation Bar
The navigation bar is a `QToolBar` positioned at the bottom of the main window (replacing or sitting alongside FreeCAD's default bottom toolbar area).
### 4.1 Layout
```
┌─────────────────────────────────────────────────────────────────────┐
│ [WB: Sketcher] │ ❶ Primitives │ ② Constraints │ ③ Dimensions │ ◀▶ │
│ │ 1:Line 2:Rect 3:Circle 4:Arc 5:Point 6:Slot ... │
└─────────────────────────────────────────────────────────────────────┘
```
- **Left section:** Current workbench name with Ctrl+N hint
- **Middle section (top row):** Command groupings, MRU ordered. Active grouping is ❶ (filled circle), others are ②③ etc. Scrollable horizontally if >9.
- **Middle section (bottom row):** Commands within the active grouping, numbered 1-9
- **Right section:** Scroll arrows for overflow groupings
### 4.2 Visual States
- **Active grouping:** Bold text, filled number badge, Catppuccin Mocha `blue` (#89b4fa) accent
- **Inactive groupings:** Normal text, outlined number badge, `surface1` (#45475a) text
- **Hovered command:** `surface2` (#585b70) background highlight
- **Active command (tool in use):** `green` (#a6e3a1) underline indicator
### 4.3 Mouse Interaction
- Click any grouping to activate it (equivalent to Shift+N)
- Click any command to execute it (equivalent to pressing N)
- Scroll wheel on grouping area to cycle through overflow groupings
- Click scroll arrows to page through overflow
---
## 5. Workbench Command Groupings
Each workbench's existing FreeCAD toolbars map to command groupings. Where a workbench has >9 toolbars, split into Tier 1 (default, most common) and Tier 2 (accessible via scrolling or `Shift+Left/Right`).
### 5.1 Sketcher (Ctrl+1)
| Grouping | Commands (1-9) |
|----------|---------------|
| Primitives | Line, Rectangle, Circle, Arc, Point, Slot, B-Spline, Polyline, Ellipse |
| Constraints | Coincident, Horizontal, Vertical, Parallel, Perpendicular, Tangent, Equal, Symmetric, Block |
| Dimensions | Distance, Horizontal Distance, Vertical Distance, Radius, Diameter, Angle, Lock, Constrain Refraction |
| Construction | Toggle Construction, External Geometry, Carbon Copy, Offset, Trim, Extend, Split |
| Tools | Mirror, Array (Linear), Array (Polar), Move, Rotate, Scale, Close Shape, Connect Edges |
### 5.2 Part Design (Ctrl+2)
| Grouping | Commands (1-9) |
|----------|---------------|
| Additive | Pad, Revolution, Additive Loft, Additive Pipe, Additive Helix, Additive Box, Additive Cylinder, Additive Sphere, Additive Cone |
| Subtractive | Pocket, Hole, Groove, Subtractive Loft, Subtractive Pipe, Subtractive Helix, Subtractive Box, Subtractive Cylinder, Subtractive Sphere |
| Datums | New Sketch, Datum Plane, Datum Line, Datum Point, Shape Binder, Sub-Shape Binder, ZTools Datum Creator, ZTools Datum Manager |
| Transformations | Mirrored, Linear Pattern, Polar Pattern, MultiTransform, ZTools Rotated Linear Pattern |
| Modeling | Fillet, Chamfer, Draft, Thickness, Boolean, ZTools Enhanced Pocket |
### 5.3 Assembly (Ctrl+3)
| Grouping | Commands (1-9) |
|----------|---------------|
| Components | Insert Component, Create Part, Create Assembly, Ground, BOM |
| Joints | Fixed, Revolute, Cylindrical, Slider, Ball, Planar, Distance, Angle, Parallel |
| Patterns | ZTools Linear Pattern, ZTools Polar Pattern |
### 5.4 Spreadsheet (Ctrl+4)
| Grouping | Commands (1-9) |
|----------|---------------|
| Editing | Merge Cells, Split Cell, Alias, Import CSV, Export CSV |
| Formatting | Bold, Italic, Underline, Align Left, Align Center, Align Right, BG Color, Text Color, Quick Alias |
### 5.5 TechDraw (Ctrl+5)
Groupings derived from TechDraw's existing toolbars at runtime.
> **Note:** The exact command lists above are initial defaults. The addon discovers available commands from each workbench's toolbar structure at activation time and falls back to these defaults only if discovery fails.
---
## 6. MRU History Behavior
### 6.1 Grouping History (per workbench)
Each workbench maintains its own grouping MRU stack.
- When a grouping is activated (via `Shift+N` or mouse click), it moves to position 0
- The previously active grouping moves to position 1, everything else shifts down
- Position 0 is always the active grouping (already selected, shown leftmost)
- `Shift+1` is a no-op (already active), `Shift+2` activates the previous grouping, etc.
### 6.2 Workbench History
- Workbenches have fixed Ctrl+N assignments (not MRU ordered)
- However, each workbench remembers its last-active grouping
- Switching to a workbench restores its last-active grouping as position 0
### 6.3 Persistence
Stored in `FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/QuickNav")`:
| Parameter | Type | Description |
|-----------|------|-------------|
| `Enabled` | Bool | Whether QuickNav is currently active |
| `GroupHistory/<Workbench>` | String | Semicolon-delimited list of grouping names in MRU order |
| `LastGrouping/<Workbench>` | String | Name of the last-active grouping per workbench |
| `CustomSlots/Ctrl6` through `Ctrl9` | String | Workbench names for user-configurable slots |
---
## 7. Auto-Execution Logic
When a command is invoked via number key, QuickNav checks if the command can be auto-executed:
### 7.1 Auto-Execute Conditions
A command auto-executes (runs and completes without entering a persistent mode) when:
1. **Pad/Pocket after closed sketch:** If the active body has a sketch that was just closed (sketch edit mode exited with a closed profile), pressing the Pad or Pocket command key creates the feature with default parameters. The task panel still opens for parameter adjustment.
2. **Boolean operations:** If exactly two bodies/shapes are selected, boolean commands execute with defaults.
3. **Constraint application:** If appropriate geometry is pre-selected in Sketcher, constraint commands apply immediately.
### 7.2 Mode-Entry (Default)
All other commands enter their standard FreeCAD tool mode — identical to clicking the toolbar button. The user interacts with the 3D view and/or task panel as normal.
---
## 8. Key Event Handling
### 8.1 Event Filter Architecture
```python
class QuickNavEventFilter(QObject):
"""Installed on FreeCAD's main window via installEventFilter().
Intercepts KeyPress events when QuickNav is active.
Passes through all events when QuickNav is inactive.
"""
def eventFilter(self, obj, event):
if event.type() != QEvent.KeyPress:
return False
if not self._active:
return False
# Don't intercept when a text input widget has focus
focused = QApplication.focusWidget()
if isinstance(focused, (QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox)):
return False
# Don't intercept when task panel input fields are focused
if self._is_task_panel_input(focused):
return False
key = event.key()
modifiers = event.modifiers()
if key == Qt.Key_0 and modifiers == Qt.NoModifier:
self.toggle_active()
return True
if key >= Qt.Key_1 and key <= Qt.Key_9:
n = key - Qt.Key_0
if modifiers == Qt.ControlModifier:
self.switch_workbench(n)
return True
elif modifiers == Qt.ShiftModifier:
self.switch_grouping(n)
return True
elif modifiers == Qt.NoModifier:
self.execute_command(n)
return True
return False # Pass through all other keys
```
### 8.2 Conflict Resolution
QuickNav's event filter takes priority when active. FreeCAD's existing keybindings for `Ctrl+1` through `Ctrl+9` (if any) are overridden while QuickNav is enabled. The original bindings are restored when QuickNav is toggled off or unloaded.
Existing `Shift+` and bare number key bindings in FreeCAD are similarly overridden only while QuickNav is active. This is safe because:
- FreeCAD does not use bare number keys as shortcuts by default
- Shift+number is not commonly bound in default FreeCAD
### 8.3 Input Widget Safety
The event filter must NOT intercept keys when the user is:
- Typing in the Python console
- Entering values in the task panel (dimensions, parameters)
- Editing spreadsheet cells
- Typing in any `QLineEdit`, `QTextEdit`, `QSpinBox`, or `QDoubleSpinBox`
- Using the Sketcher's inline dimension input
---
## 9. Addon Structure
```
mods/quicknav/
├── package.xml # FreeCAD addon manifest with <kindred> extension
├── Init.py # Non-GUI initialization (no-op)
├── InitGui.py # Registers QuickNavWorkbench
├── quicknav/
│ ├── __init__.py
│ ├── core.py # QuickNavManager singleton — orchestrates state
│ ├── event_filter.py # QuickNavEventFilter (QObject)
│ ├── nav_bar.py # NavigationBar (QToolBar subclass)
│ ├── workbench_map.py # Fixed workbench → Ctrl+N mapping + grouping discovery
│ ├── history.py # MRU stack with ParamGet persistence
│ ├── auto_exec.py # Auto-execution condition checks
│ ├── commands.py # FreeCAD command wrappers (QuickNav_Toggle, etc.)
│ └── resources/
│ ├── icons/ # Number badge SVGs, QuickNav icon
│ └── theme.py # Catppuccin Mocha color tokens
└── tests/
└── test_history.py # MRU stack unit tests
```
### 9.1 Manifest
```xml
<?xml version="1.0" encoding="UTF-8"?>
<package format="1">
<name>QuickNav</name>
<description>Keyboard-driven toolbar navigation</description>
<version>0.1.0</version>
<maintainer email="dev@kindred-systems.com">Kindred Systems</maintainer>
<license>LGPL-2.1</license>
<content>
<workbench>
<classname>QuickNavWorkbench</classname>
</workbench>
</content>
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>10</load_priority>
<pure_python>true</pure_python>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
</kindred>
</package>
```
### 9.2 Activation
QuickNav activates when its workbench is loaded (via the addon loader or manual activation). It installs the event filter on the main window and creates the navigation bar. The workbench itself is invisible — it does not add its own toolbars or menus beyond the navigation bar. It acts as a transparent overlay on whatever workbench the user is actually working in.
```python
class QuickNavWorkbench(Gui.Workbench):
"""Invisible workbench that installs QuickNav on load.
QuickNav doesn't replace the active workbench — it layers on top.
Loading QuickNav installs the event filter and nav bar, then
immediately re-activates the previously active workbench.
"""
def Initialize(self):
QuickNavManager.instance().install()
def Activated(self):
# Re-activate the previous workbench so QuickNav is transparent
prev = QuickNavManager.instance().previous_workbench
if prev:
Gui.activateWorkbench(prev)
def Deactivated(self):
pass
def GetClassName(self):
return "Gui::PythonWorkbench"
```
**Alternative (preferred for Create):** Instead of a workbench, QuickNav can be activated directly from `Create/InitGui.py` at boot, gated by the `Enabled` preference. This avoids the workbench-switching dance entirely. The `QuickNavWorkbench` registration is kept for standalone FreeCAD compatibility.
---
## 10. Command Discovery
At activation time, QuickNav introspects each workbench's toolbars to build the command grouping map.
```python
def discover_groupings(workbench_name: str) -> list[CommandGrouping]:
"""Discover command groupings from a workbench's toolbar structure.
1. Temporarily activate the workbench (if not already active)
2. Enumerate QToolBars from the main window
3. Map toolbar name → list of QAction names
4. Filter out non-command actions (separators, widgets)
5. Split into tiers if >9 groupings
6. Restore the previously active workbench
"""
```
### 10.1 Fallback Defaults
If toolbar discovery fails (workbench not initialized, empty toolbars), QuickNav falls back to the hardcoded groupings in Section 5. These are stored as a Python dict in `workbench_map.py`.
### 10.2 ZTools Integration
ZTools commands injected via `WorkbenchManipulator` appear in the discovered toolbars and are automatically included in the relevant groupings. No special handling is needed — QuickNav discovers commands after all manipulators have run.
---
## 11. FreeCAD Compatibility
QuickNav is designed as a standalone FreeCAD addon that works without Kindred Create or the SDK.
| Feature | FreeCAD | Kindred Create |
|---------|---------|----------------|
| Core navigation (keys, nav bar) | ✅ | ✅ |
| Catppuccin Mocha theming | ❌ (uses Qt defaults) | ✅ (via SDK theme tokens) |
| Auto-boot on startup | ❌ (manual workbench activation) | ✅ (via addon loader) |
| ZTools commands in groupings | ❌ (not present) | ✅ (discovered from manipulated toolbars) |
The SDK dependency is optional — QuickNav checks for `kindred_sdk` availability and degrades gracefully:
```python
try:
from kindred_sdk.theme import get_theme_tokens
THEME = get_theme_tokens()
except ImportError:
THEME = None # Use Qt default palette
```
---
## 12. Implementation Phases
### Phase 1: Core Infrastructure
- Event filter with key interception and input widget safety
- QuickNavManager singleton with toggle on/off
- Navigation bar widget (QToolBar) with basic layout
- Hardcoded workbench/grouping maps from Section 5
- ParamGet persistence for enabled state
### Phase 2: Dynamic Discovery
- Toolbar introspection for command grouping discovery
- MRU history with persistence
- Grouping overflow scrolling
- Workbench restore (last-active grouping per workbench)
### Phase 3: Auto-Execution
- Context-aware auto-execute logic
- Sketcher closed-profile detection for Pad/Pocket
- Pre-selection constraint application
### Phase 4: Polish
- Number badge SVG icons
- Catppuccin Mocha theming (conditional on SDK)
- Scroll animations
- Settings dialog (custom Ctrl+6-9 assignments)
- FreeCAD standalone packaging
---
## 13. Open Questions
1. **Tier switching UX:** When a workbench has >9 groupings split into tiers, should `Shift+0` toggle between tiers, or should tiers be purely a scroll/mouse concept?
2. **Visual number badges:** Should the commands in the nav bar show keycap-style badges (like `⌨ 1`) or just prepend the number (`1: Line`)?
3. **Sketcher inline dimension input:** FreeCAD's Sketcher has an inline dimension entry that isn't a standard QLineEdit. Need to verify the event filter correctly identifies and skips this widget.
4. **Ctrl+N conflicts with Create shortcuts:** Verify that Create/Silo don't already bind Ctrl+1 through Ctrl+9. The Silo toggle uses Ctrl+O/S/N, so these should be clear.

View File

@@ -1,6 +1,6 @@
# Configuration Reference
**Last Updated:** 2026-02-06
**Last Updated:** 2026-03-01
---
@@ -153,6 +153,70 @@ odoo:
---
## Approval Workflows
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `workflows.directory` | string | `"/etc/silo/workflows"` | Path to directory containing YAML workflow definition files |
Workflow definition files describe multi-stage approval processes using a state machine pattern. Each file defines a workflow with states, transitions, and approver requirements.
```yaml
workflows:
directory: "/etc/silo/workflows"
```
---
## Solver
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `solver.default_solver` | string | `""` | `SILO_SOLVER_DEFAULT` | Default solver backend name |
| `solver.max_context_size_mb` | int | `10` | — | Maximum SolveContext payload size in MB |
| `solver.default_timeout` | int | `300` | — | Default solver job timeout in seconds |
| `solver.auto_diagnose_on_commit` | bool | `false` | — | Auto-submit diagnose job on assembly revision commit |
The solver module depends on the `jobs` module being enabled. See [SOLVER.md](SOLVER.md) for the full solver service specification.
```yaml
solver:
default_solver: "ondsel"
max_context_size_mb: 10
default_timeout: 300
auto_diagnose_on_commit: true
```
---
## Modules
Optional module toggles. Each module can be explicitly enabled or disabled. If not listed, the module's built-in default applies. See [MODULES.md](MODULES.md) for the full module system specification.
```yaml
modules:
projects:
enabled: true
audit:
enabled: true
odoo:
enabled: false
freecad:
enabled: true
jobs:
enabled: false
dag:
enabled: false
solver:
enabled: false
sessions:
enabled: true
```
The `auth.enabled` field controls the `auth` module directly (not duplicated under `modules:`). The `sessions` module depends on `auth` and is enabled by default.
---
## 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.
@@ -271,6 +335,7 @@ All environment variable overrides. These take precedence over values in `config
| `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 |
| `SILO_SOLVER_DEFAULT` | `solver.default_solver` | Default solver backend name |
Additionally, YAML values can reference environment variables directly using `${VAR_NAME}` syntax, which is expanded at load time via `os.ExpandEnv()`.

View File

@@ -1,6 +1,6 @@
# Silo Gap Analysis
**Date:** 2026-02-13
**Date:** 2026-03-01
**Status:** Analysis Complete (Updated)
---
@@ -130,8 +130,8 @@ FreeCAD workbench maintained in separate [silo-mod](https://git.kindred-systems.
|-----|-------------|--------|--------|
| ~~**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 |
| **No locking** | No concurrent edit protection | Multi-user unsafe | Partial (edit sessions with hard interference detection; full pessimistic locking not yet implemented) |
| ~~**No approval workflow**~~ | ~~No release/sign-off process~~ | ~~Quality control gap~~ | **Implemented** (YAML-configurable ECO workflows, multi-stage review gates, digital signatures) |
### 3.2 Important Gaps
@@ -355,47 +355,54 @@ These design decisions remain unresolved:
## Appendix A: File Structure
Revision endpoints, status, labels, authentication, audit logging, and file attachments are implemented. Current structure:
Current structure:
```
internal/
api/
approval_handlers.go # Approval/ECO workflow endpoints
audit_handlers.go # Audit/completeness endpoints
auth_handlers.go # Login, tokens, OIDC
bom_handlers.go # Flat BOM, cost roll-up
broker.go # SSE broker with targeted delivery
dag_handlers.go # Dependency DAG endpoints
dependency_handlers.go # .kc dependency resolution
file_handlers.go # Presigned uploads, item files, thumbnails
handlers.go # Items, schemas, projects, revisions
handlers.go # Items, schemas, projects, revisions, Server struct
job_handlers.go # Job queue endpoints
location_handlers.go # Location hierarchy endpoints
macro_handlers.go # .kc macro endpoints
metadata_handlers.go # .kc metadata endpoints
middleware.go # Auth middleware
odoo_handlers.go # Odoo integration endpoints
routes.go # Route registration (78 endpoints)
pack_handlers.go # .kc checkout packing
routes.go # Route registration (~140 endpoints)
runner_handlers.go # Job runner endpoints
search.go # Fuzzy search
session_handlers.go # Edit session acquire/release/query
settings_handlers.go # Admin settings endpoints
solver_handlers.go # Solver service endpoints
sse_handler.go # SSE event stream handler
workstation_handlers.go # Workstation registration
auth/
auth.go # Auth service: local, LDAP, OIDC
db/
edit_sessions.go # Edit session repository
items.go # Item and revision repository
item_files.go # File attachment repository
relationships.go # BOM repository
jobs.go # Job queue repository
projects.go # Project repository
relationships.go # BOM repository
workstations.go # Workstation repository
modules/
modules.go # Module registry (12 modules)
loader.go # Config-to-module state loader
storage/
storage.go # 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
023_edit_sessions.sql # Edit session tracking (latest)
```
---
@@ -465,28 +472,28 @@ This section compares Silo's capabilities against SOLIDWORKS PDM features. Gaps
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| Check-in/check-out | Full pessimistic locking | None | High | Moderate |
| Check-in/check-out | Full pessimistic locking | Partial (edit sessions with hard interference) | 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 |
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.
Silo has edit sessions with hard interference detection (unique index on item + context_level + object_id prevents two users from editing the same object simultaneously). Full pessimistic file-level locking is not yet implemented. Visual diff comparison would require FreeCAD integration for CAD file visualization.
### C.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 |
| Custom workflows | Full visual designer | Full (YAML-defined state machines) | - | - |
| State transitions | Configurable with permissions | Full (configurable transition rules) | - | - |
| Parallel approvals | Multiple approvers required | Full (multi-stage review gates) | - | - |
| 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 |
| ECO process | Built-in change management | Full (YAML-configurable ECO workflows) | - | - |
| Child state conditions | Block parent if children invalid | None | Medium | Moderate |
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.
Workflow management has been significantly addressed. Silo now supports YAML-defined state machine workflows with configurable transitions, multi-stage approval gates, and digital signatures. Remaining gaps: automatic timer-based transitions, email notifications, and child state condition enforcement.
### C.3 User Management & Security
@@ -549,13 +556,13 @@ CAD integration is maintained in separate repositories ([silo-mod](https://git.k
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| ERP integration | SAP, Dynamics, etc. | Partial (Odoo stubs) | Medium | Complex |
| API access | Full COM/REST API | Full REST API (78 endpoints) | - | - |
| API access | Full COM/REST API | Full REST API (~140 endpoints) | - | - |
| Dispatch scripts | Automation without coding | None | Medium | Moderate |
| Task scheduler | Background processing | None | Medium | Moderate |
| Task scheduler | Background processing | Full (job queue with runners) | - | - |
| Email system | SMTP integration | None | High | Simple |
| Web portal | Browser access | Full (React SPA + auth) | - | - |
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.
Silo has a comprehensive REST API (~140 endpoints) and a full web UI with authentication. Odoo ERP integration has config/sync-log scaffolding but push/pull operations are stubs. Job queue with runner management is fully implemented. Remaining gaps: email notifications, dispatch automation.
### C.8 Reporting & Analytics
@@ -586,13 +593,13 @@ File storage works well. Thumbnail generation and file preview would significant
| Category | Feature | SW PDM Standard | SW PDM Pro | Silo Current | Silo Planned |
|----------|---------|-----------------|------------|--------------|--------------|
| **Version Control** | Check-in/out | Yes | Yes | No | Tier 1 |
| **Version Control** | Check-in/out | Yes | Yes | Partial (edit sessions) | Tier 1 |
| | 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 | Tier 4 |
| | Parallel approval | No | Yes | No | Tier 4 |
| **Workflow** | Custom workflows | Limited | Yes | Yes (YAML state machines) | - |
| | Parallel approval | No | Yes | Yes (multi-stage gates) | - |
| | Notifications | No | Yes | No | Tier 1 |
| **Security** | User auth | Windows | Windows/LDAP | Yes (local, LDAP, OIDC) | - |
| | Permissions | Basic | Granular | Partial (role-based) | Tier 4 |
@@ -606,7 +613,7 @@ File storage works well. Thumbnail generation and file preview would significant
| **Data** | CSV import/export | Yes | Yes | Yes | - |
| | ODS import/export | No | No | Yes | - |
| | Project management | Yes | Yes | Yes | - |
| **Integration** | API | Limited | Full | Full REST (78) | - |
| **Integration** | API | Limited | Full | Full REST (~140) | - |
| | ERP connectors | No | Yes | Partial (Odoo stubs) | Tier 6 |
| | Web access | No | Yes | Yes (React SPA + auth) | - |
| **Files** | Versioning | Yes | Yes | Yes | - |

View File

@@ -491,4 +491,7 @@ After a successful installation:
| [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 |
| [MODULES.md](MODULES.md) | Module system specification |
| [WORKERS.md](WORKERS.md) | Job queue and runner system |
| [SOLVER.md](SOLVER.md) | Assembly solver service |
| [COMPONENT_AUDIT.md](COMPONENT_AUDIT.md) | Component audit tool design |

View File

@@ -1,7 +1,7 @@
# Module System Specification
**Status:** Draft
**Last Updated:** 2026-02-14
**Last Updated:** 2026-03-01
---
@@ -36,6 +36,8 @@ These cannot be disabled. They define what Silo *is*.
| `freecad` | Create Integration | `true` | URI scheme, executable path, client settings |
| `jobs` | Job Queue | `false` | Async compute jobs, runner management |
| `dag` | Dependency DAG | `false` | Feature DAG sync, validation states, interference detection |
| `solver` | Solver | `false` | Assembly constraint solving via server-side runners |
| `sessions` | Sessions | `true` | Workstation registration, edit sessions, and presence tracking |
### 2.3 Module Dependencies
@@ -46,6 +48,8 @@ Some modules require others to function:
| `dag` | `jobs` |
| `jobs` | `auth` (runner tokens) |
| `odoo` | `auth` |
| `solver` | `jobs` |
| `sessions` | `auth` |
When enabling a module, its dependencies are validated. The server rejects enabling `dag` without `jobs`. Disabling a module that others depend on shows a warning listing dependents.
@@ -257,6 +261,34 @@ PUT /api/items/{partNumber}/dag
POST /api/items/{partNumber}/dag/mark-dirty/{nodeKey}
```
### 3.11 `solver`
```
GET /api/solver/jobs
GET /api/solver/jobs/{jobID}
POST /api/solver/jobs
POST /api/solver/jobs/{jobID}/cancel
GET /api/solver/solvers
GET /api/solver/results/{partNumber}
```
### 3.12 `sessions`
```
# Workstation management
GET /api/workstations
POST /api/workstations
DELETE /api/workstations/{workstationID}
# Edit sessions (user-scoped)
GET /api/edit-sessions
# Edit sessions (item-scoped)
GET /api/items/{partNumber}/edit-sessions
POST /api/items/{partNumber}/edit-sessions
DELETE /api/items/{partNumber}/edit-sessions/{sessionID}
```
---
## 4. Disabled Module Behavior
@@ -431,6 +463,18 @@ GET /api/modules
"required": false,
"name": "Dependency DAG",
"depends_on": ["jobs"]
},
"solver": {
"enabled": false,
"required": false,
"name": "Solver",
"depends_on": ["jobs"]
},
"sessions": {
"enabled": true,
"required": false,
"name": "Sessions",
"depends_on": ["auth"]
}
},
"server": {
@@ -518,7 +562,9 @@ Returns full config grouped by module with secrets redacted:
"job_timeout_check": 30,
"default_priority": 100
},
"dag": { "enabled": false }
"dag": { "enabled": false },
"solver": { "enabled": false, "default_solver": "ondsel" },
"sessions": { "enabled": true }
}
```
@@ -632,6 +678,11 @@ modules:
default_priority: 100
dag:
enabled: false
solver:
enabled: false
default_solver: ondsel
sessions:
enabled: true
```
If a module is not listed under `modules:`, its default enabled state from Section 2.2 applies. The `auth.enabled` field continues to control the `auth` module (no duplication under `modules:`).
@@ -732,6 +783,7 @@ These are read-only in the UI (setup-only via YAML/env). The "Test" button is av
- **Per-module permissions** — beyond the current role hierarchy, modules may define fine-grained scopes (e.g., `jobs:admin`, `dag:write`).
- **Location & Inventory module** — when the Location/Inventory API is implemented (tables already exist), it becomes a new optional module.
- **Notifications module** — per ROADMAP.md Tier 1, notifications/subscriptions will be a dedicated module.
- **Soft interference detection** — the `sessions` module currently enforces hard interference (unique index on item + context_level + object_id). Soft interference detection (overlapping dependency cones) is planned as a follow-up.
---

View File

@@ -0,0 +1,248 @@
# Projects & Approvals — Implementation Reference
## Projects
### Database Schema
**Migration 006 + 009**
```sql
-- projects table
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(10) UNIQUE NOT NULL,
name TEXT,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by TEXT -- added in migration 009
);
-- many-to-many junction
CREATE TABLE item_projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(item_id, project_id)
);
```
Items can belong to multiple projects. Project codes are immutable after creation.
### API Endpoints
All gated by `RequireModule("projects")`.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/projects` | viewer | List all projects |
| POST | `/api/projects` | editor | Create project (code: 2-10 alphanum) |
| GET | `/api/projects/{code}` | viewer | Get single project |
| PUT | `/api/projects/{code}` | editor | Update name/description |
| DELETE | `/api/projects/{code}` | editor | Delete project (cascades associations) |
| GET | `/api/projects/{code}/items` | viewer | List non-archived items in project |
| GET | `/api/projects/{code}/sheet.ods` | viewer | Export ODS workbook (items + BOMs) |
| GET | `/api/items/{pn}/projects` | viewer | Get projects for an item |
| POST | `/api/items/{pn}/projects` | editor | Add item to projects (`{"projects":["A","B"]}`) |
| DELETE | `/api/items/{pn}/projects/{code}` | editor | Remove item from project |
### Backend Files
| File | Contents |
|------|----------|
| `internal/db/projects.go` | `ProjectRepository` — 13 methods: List, GetByCode, GetByID, Create, Update, Delete, AddItemToProject, AddItemToProjectByCode, RemoveItemFromProject, RemoveItemFromProjectByCode, GetProjectsForItem, GetProjectCodesForItem, GetItemsForProject, SetItemProjects |
| `internal/api/handlers.go` | Handlers: HandleListProjects, HandleCreateProject, HandleGetProject, HandleUpdateProject, HandleDeleteProject, HandleGetProjectItems, HandleGetItemProjects, HandleAddItemProjects, HandleRemoveItemProject |
| `internal/api/ods.go` | HandleProjectSheetODS — multi-sheet ODS export with items list + per-assembly BOM sheets |
| `internal/api/routes.go` | Route registration under `/api/projects` and `/api/items/{pn}/projects` |
### Frontend
**Page:** `web/src/pages/ProjectsPage.tsx` at route `/projects`
Features:
- Sortable table (code, name, description, item count, created date)
- Create form with code validation (2-10 chars, auto-uppercase)
- Inline edit (name/description only, code immutable)
- Delete with confirmation dialog
- Item count fetched per-project in parallel
- Catppuccin Mocha theme, conditionally shown in sidebar when module enabled
### Config
```yaml
modules:
projects:
enabled: true # default true
```
### Not Yet Implemented
- Project-level permissions / ownership (all projects visible to all viewers)
- Project-based filtering in the main items list page
- Bulk item operations across projects
- Project archival / soft-delete
- Project hierarchies or team assignment
---
## Approvals & ECO Workflows
### Database Schema
**Migration 018 + 019**
```sql
-- item_approvals table
CREATE TABLE item_approvals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
workflow_name TEXT, -- added in migration 019
eco_number TEXT,
state TEXT NOT NULL DEFAULT 'draft',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by TEXT
);
-- approval_signatures table
CREATE TABLE approval_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
approval_id UUID NOT NULL REFERENCES item_approvals(id) ON DELETE CASCADE,
username TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
signed_at TIMESTAMPTZ,
comment TEXT
);
```
### State Machine
**Approval states:** `draft` | `pending` | `approved` | `rejected`
**Signature states:** `pending` | `approved` | `rejected`
**Auto-transitions** (evaluated after each signature):
1. If any signature is `rejected` and workflow defines `any_reject` rule -> approval becomes `rejected`
2. If all signatures on required gates are `approved` and workflow defines `all_required_approve` rule -> approval becomes `approved`
3. Otherwise -> stays `pending`
### Workflow Definitions
YAML files in `workflows/` directory, loaded at server startup.
**`workflows/engineering-change.yaml`:**
```yaml
workflow:
name: engineering-change
version: 1
description: "Standard engineering change order with peer review and manager approval"
states: [draft, pending, approved, rejected]
gates:
- role: engineer
label: "Peer Review"
required: true
- role: manager
label: "Manager Approval"
required: true
- role: quality
label: "Quality Sign-off"
required: false
rules:
any_reject: rejected
all_required_approve: approved
```
**`workflows/quick-review.yaml`:**
```yaml
workflow:
name: quick-review
version: 1
description: "Single reviewer approval for minor changes"
states: [draft, pending, approved, rejected]
gates:
- role: reviewer
label: "Review"
required: true
rules:
any_reject: rejected
all_required_approve: approved
```
Custom workflows: add a `.yaml` file to the workflows directory with the same structure.
### API Endpoints
Not module-gated (always available when server is running).
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/workflows` | viewer | List all loaded workflow definitions |
| GET | `/api/items/{pn}/approvals` | viewer | List approvals for item (with signatures) |
| POST | `/api/items/{pn}/approvals` | editor | Create approval with signers |
| POST | `/api/items/{pn}/approvals/{id}/sign` | editor | Record signature (approve/reject) |
**Create approval request:**
```json
{
"workflow": "engineering-change",
"eco_number": "ECO-2026-042",
"signers": [
{ "username": "alice", "role": "engineer" },
{ "username": "bob", "role": "manager" },
{ "username": "carol", "role": "quality" }
]
}
```
Validation: workflow must exist, all signer roles must be defined in the workflow, all required gates must have at least one signer.
**Sign request:**
```json
{
"status": "approved",
"comment": "Looks good"
}
```
Validation: approval must be in `pending` state, caller must be a listed signer, caller must not have already signed.
### SSE Events
Published on the existing `GET /api/events` stream.
| Event | Payload | Trigger |
|-------|---------|---------|
| `approval.created` | `{part_number, approval_id, workflow, eco_number}` | Approval created |
| `approval.signed` | `{part_number, approval_id, username, status}` | Signature recorded |
| `approval.completed` | `{part_number, approval_id, state}` | All required gates resolved |
### Backend Files
| File | Contents |
|------|----------|
| `internal/db/item_approvals.go` | `ItemApprovalRepository` — Create, AddSignature, GetWithSignatures, ListByItemWithSignatures, UpdateState, GetSignatureForUser, UpdateSignature |
| `internal/api/approval_handlers.go` | HandleGetApprovals, HandleCreateApproval, HandleSignApproval, HandleListWorkflows, evaluateApprovalState |
| `internal/workflow/workflow.go` | Workflow/Gate/Rules types, Load, LoadAll, Validate, RequiredGates, HasRole |
| `workflows/engineering-change.yaml` | 3-gate ECO workflow |
| `workflows/quick-review.yaml` | 1-gate quick review workflow |
### Config
```yaml
workflows:
directory: /etc/silo/workflows # path to workflow YAML files
```
### Frontend
**No approval UI exists.** The backend API is fully functional but has no web interface. Issue #147 includes an approvals page scaffold as part of the .kc metadata web UI phase.
### Not Yet Implemented
- Web UI for creating, viewing, and signing approvals
- Approval history / audit log view
- Lifecycle state transitions tied to approval outcomes (e.g. auto-release on approval)
- Email or notification integration for pending signatures
- Delegation / proxy signing
- Approval templates (pre-filled signer lists per workflow)
- Bulk approval operations

View File

@@ -92,7 +92,7 @@ Everything depends on these. They define what Silo *is*.
| **API Endpoint Registry** | Module discovery, dynamic UI rendering, health checks | Not Started |
| **Web UI Shell** | App launcher, breadcrumbs, view framework, module rendering | Partial |
| **Python Scripting Engine** | Server-side hook execution, module extension point | Not Started |
| **Job Queue Infrastructure** | Redis/NATS shared async service for all compute modules | Not Started |
| **Job Queue Infrastructure** | PostgreSQL-backed async job queue with runner management | Complete |
### Tier 1 -- Core Services
@@ -102,7 +102,7 @@ Broad downstream dependencies. These should be built early because retrofitting
|--------|-------------|------------|--------|
| **Headless Create** | API-driven FreeCAD instance for file manipulation, geometry queries, format conversion, rendering | Core Silo, Job Queue | Not Started |
| **Notifications & Subscriptions** | Per-part watch lists, lifecycle event hooks, webhook delivery | Core Silo, Registry | Not Started |
| **Audit Trail / Compliance** | ITAR, ISO 9001, AS9100 traceability; module-level event journaling | Core Silo | Partial |
| **Audit Trail / Compliance** | ITAR, ISO 9001, AS9100 traceability; module-level event journaling | Core Silo | Complete (base) |
### Tier 2 -- File Intelligence & Collaboration
@@ -132,7 +132,7 @@ Process modules that formalize how engineering work moves through an organizatio
| Module | Description | Depends On | Status |
|--------|-------------|------------|--------|
| **Approval / ECO Workflow** | Engineering change orders, multi-stage review gates, digital signatures | Notifications, Audit Trail, Schemas | Not Started |
| **Approval / ECO Workflow** | Engineering change orders, multi-stage review gates, digital signatures | Notifications, Audit Trail, Schemas | Complete |
| **Shop Floor Drawing Distribution** | Controlled push-to-production drawings; web-based appliance displays on the floor | Headless Create, Approval Workflow | Not Started |
| **Import/Export Bridge** | STEP, IGES, 3MF connectors; SOLIDWORKS migration tooling; ERP adapters | Headless Create | Not Started |
| **Multi-tenant / Org Management** | Org boundaries, role-based permissioning, storage quotas | Core Auth, Audit Trail | Not Started |
@@ -202,15 +202,15 @@ Implement engineering change processes (Tier 4: Approval/ECO Workflow).
| Task | Description | Status |
|------|-------------|--------|
| Workflow designer | YAML-defined state machines | Not Started |
| State transitions | Configurable transition rules with permissions | Not Started |
| Approval workflows | Single and parallel approver gates | Not Started |
| Workflow designer | YAML-defined state machines | Complete |
| State transitions | Configurable transition rules with permissions | Complete |
| Approval workflows | Single and parallel approver gates | Complete |
| Email notifications | SMTP integration for alerts on state changes | Not Started |
**Success metrics:**
- Engineering change process completable in Silo
- ~~Engineering change process completable in Silo~~ Done (YAML-configured workflows with multi-stage gates)
- Email notifications delivered reliably
- Workflow state visible in web UI
- ~~Workflow state visible in web UI~~ Available via API
### Search & Discovery
@@ -240,9 +240,17 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
5. ~~Multi-level BOM API~~ -- recursive expansion with configurable depth
6. ~~BOM export~~ -- CSV and ODS formats
### Recently Completed
7. ~~Workflow engine~~ -- YAML-defined state machines with multi-stage approval gates
8. ~~Job queue~~ -- PostgreSQL-backed async compute with runner management
9. ~~Assembly solver service~~ -- server-side constraint solving with result caching
10. ~~Workstation registration~~ -- device identity and heartbeat tracking
11. ~~Edit sessions~~ -- acquire/release with hard interference detection
### Critical Gaps (Required for Team Use)
1. **Workflow engine** -- state machines with transitions and approvals
1. ~~**Workflow engine**~~ -- Complete (YAML-configured approval workflows)
2. **Check-out locking** -- pessimistic locking for CAD files
### High Priority Gaps (Significant Value)
@@ -275,7 +283,7 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
1. **Module manifest format** -- JSON, TOML, or Python-based? Tradeoffs between simplicity and expressiveness.
2. **.kc thumbnail policy** -- Single canonical thumbnail vs. multi-view renders. Impacts file size and generation cost.
3. **Job queue technology** -- Redis Streams vs. NATS. Redis is already in the stack; NATS offers better pub/sub semantics for event-driven modules.
3. ~~**Job queue technology**~~ -- Resolved: PostgreSQL-backed with `SELECT FOR UPDATE SKIP LOCKED` for exactly-once delivery. No external queue dependency.
4. **Headless Create deployment** -- Sidecar container per Silo instance, or pool of workers behind the job queue?
5. **BIM-MES workbench scope** -- How much of FreeCAD BIM is reusable vs. needs to be purpose-built for inventory/facility modeling?
6. **Offline .kc workflow** -- How much of the `silo/` metadata is authoritative when disconnected? Reconciliation strategy on reconnect.
@@ -287,7 +295,7 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
### Implemented Features (MVP Complete)
#### Core Database System
- PostgreSQL schema with 13 migrations
- PostgreSQL schema with 23 migrations
- UUID-based identifiers throughout
- Soft delete support via `archived_at` timestamps
- Atomic sequence generation for part numbers
@@ -340,7 +348,7 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
- Template generation for import formatting
#### API & Web Interface
- REST API with 78 endpoints
- REST API with ~140 endpoints
- Authentication: local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak
- Role-based access control (admin > editor > viewer)
- API token management (SHA-256 hashed)
@@ -371,7 +379,7 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
| 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 | 11 Go test files across api, db, ods, partnum, schema packages |
| Unit tests | Partial | 31 Go test files across api, db, modules, ods, partnum, schema packages |
---

View File

@@ -1,8 +1,9 @@
# Solver Service Specification
**Status:** Draft
**Last Updated:** 2026-02-19
**Status:** Phase 3b Implemented (server endpoints, job definitions, result cache)
**Last Updated:** 2026-03-01
**Depends on:** KCSolve Phase 1 (PR #297), Phase 2 (PR #298)
**Prerequisite infrastructure:** Job queue, runner system, and SSE broadcasting are fully implemented (see [WORKERS.md](WORKERS.md), migration `015_jobs_runners.sql`, `cmd/silorunner/`).
---
@@ -38,7 +39,7 @@ This specification describes how the existing KCSolve client-side API (C++ libra
│ (SolveContext JSON)
│ 4. GET /api/events (SSE)
solver.progress, solver.completed
job.progress, job.completed
┌─────────────────────┐
│ Silo Server │
@@ -64,8 +65,8 @@ This specification describes how the existing KCSolve client-side API (C++ libra
| Component | Role | Deployment |
|-----------|------|------------|
| **Silo server** | Job queue management, REST API, SSE broadcast, result storage | Existing `silod` binary |
| **Solver runner** | Claims solver jobs, executes `kcsolve`, reports results | New runner tag `solver` on existing `silorunner` |
| **Silo server** | Job queue management, REST API, SSE broadcast, result storage | Existing `silod` binary (jobs module, migration 015) |
| **Solver runner** | Claims solver jobs, executes `kcsolve`, reports results | Existing `silorunner` binary (`cmd/silorunner/`) with `solver` tag |
| **kcsolve module** | Python/C++ solver library (Phase 1+2) | Installed on runner nodes |
| **Create client** | Submits jobs, receives results via SSE | Existing FreeCAD client |
@@ -394,28 +395,30 @@ Solver jobs emit events on the existing `/api/events` SSE stream.
### 5.1 Event Types
Solver jobs use the existing `job.*` SSE event prefix (see [WORKERS.md](WORKERS.md)). Clients filter on `definition_name` to identify solver-specific events.
| Event | Payload | When |
|-------|---------|------|
| `solver.created` | `{job_id, operation, solver, item_part_number}` | Job submitted |
| `solver.claimed` | `{job_id, runner_id, runner_name}` | Runner starts work |
| `solver.progress` | `{job_id, progress, message}` | Progress update (0-100) |
| `solver.completed` | `{job_id, status, dof, diagnostics_count, solve_time_ms}` | Job succeeded |
| `solver.failed` | `{job_id, error_message}` | Job failed |
| `job.created` | `{job_id, definition_name, trigger, item_id}` | Job submitted |
| `job.claimed` | `{job_id, runner_id, runner}` | Runner claims work |
| `job.progress` | `{job_id, progress, message}` | Progress update (0-100) |
| `job.completed` | `{job_id, runner_id}` | Job succeeded |
| `job.failed` | `{job_id, runner_id, error}` | Job failed |
### 5.2 Example Stream
```
event: solver.created
data: {"job_id":"abc-123","operation":"solve","solver":"ondsel","item_part_number":"ASM-001"}
event: job.created
data: {"job_id":"abc-123","definition_name":"assembly-solve","trigger":"manual","item_id":"uuid-..."}
event: solver.claimed
data: {"job_id":"abc-123","runner_id":"r1","runner_name":"solver-worker-01"}
event: job.claimed
data: {"job_id":"abc-123","runner_id":"r1","runner":"solver-worker-01"}
event: solver.progress
event: job.progress
data: {"job_id":"abc-123","progress":50,"message":"Building constraint system..."}
event: solver.completed
data: {"job_id":"abc-123","status":"Success","dof":3,"diagnostics_count":1,"solve_time_ms":127.4}
event: job.completed
data: {"job_id":"abc-123","runner_id":"r1"}
```
### 5.3 Client Integration
@@ -423,8 +426,8 @@ data: {"job_id":"abc-123","status":"Success","dof":3,"diagnostics_count":1,"solv
The Create client subscribes to the SSE stream and updates the Assembly workbench UI:
1. **Silo viewport widget** shows job status indicator (pending/running/done/failed)
2. On `solver.completed`, the client can fetch the full result via `GET /api/solver/jobs/{id}` and apply placements
3. On `solver.failed`, the client shows the error in the report panel
2. On `job.completed` (where `definition_name` starts with `assembly-`), the client fetches the full result via `GET /api/jobs/{id}` and applies placements
3. On `job.failed`, the client shows the error in the report panel
4. Diagnostic results (redundant/conflicting constraints) surface in the constraint tree
---
@@ -433,7 +436,9 @@ The Create client subscribes to the SSE stream and updates the Assembly workbenc
### 6.1 Runner Requirements
Solver runners are standard `silorunner` instances with the `solver` tag. They require:
Solver runners are standard `silorunner` instances (see `cmd/silorunner/main.go`) registered with the `solver` tag. The existing runner binary already handles the full job lifecycle (claim, start, progress, complete/fail, log, DAG sync). Solver support requires adding `solver-run`, `solver-diagnose`, and `solver-kinematic` to the runner's command dispatch (currently handles `create-validate`, `create-export`, `create-dag-extract`, `create-thumbnail`).
Additional requirements on the runner host:
- Python 3.11+ with `kcsolve` module installed
- `libKCSolve.so` and solver backend libraries (e.g. `libOndselSolver.so`)
@@ -453,21 +458,28 @@ curl -X POST https://silo.example.com/api/runners \
{"id":"uuid","token":"silo_runner_xyz..."}
```
### 6.3 Runner Heartbeat
### 6.3 Runner Heartbeat and Capabilities
Runners report solver capabilities during heartbeat:
The existing heartbeat endpoint (`POST /api/runner/heartbeat`) takes no body — it updates `last_heartbeat` on every authenticated request via the `RequireRunnerAuth` middleware. Runners that go 90 seconds without a request are marked offline by the background sweeper.
```json
POST /api/runner/heartbeat
{
"capabilities": {
"solvers": ["ondsel"],
"api_version": 1,
"python_version": "3.11.11"
}
}
Solver capabilities are reported via the runner's `metadata` JSONB field, set at registration time:
```bash
curl -X POST https://silo.example.com/api/runners \
-H "Authorization: Bearer admin_token" \
-d '{
"name": "solver-01",
"tags": ["solver"],
"metadata": {
"solvers": ["ondsel"],
"api_version": 1,
"python_version": "3.11.11"
}
}'
```
> **Future enhancement:** The heartbeat endpoint could be extended to accept an optional body for dynamic capability updates, but currently capabilities are static per registration.
### 6.4 Runner Execution Flow
```python
@@ -529,7 +541,9 @@ The `kcsolve.runner` module reads JSON from stdin, executes the solve, and write
### 7.1 Manual Solve Job
Triggered by the client when the user requests a server-side solve:
Triggered by the client when the user requests a server-side solve.
> **Note:** The `compute.type` uses `custom` because the valid types in `internal/jobdef/jobdef.go` are: `validate`, `rebuild`, `diff`, `export`, `custom`. Solver commands are dispatched by the runner based on the `command` field.
```yaml
job:
@@ -544,7 +558,7 @@ job:
type: assembly
compute:
type: solver
type: custom
command: solver-run
runner:
@@ -574,7 +588,7 @@ job:
type: assembly
compute:
type: solver
type: custom
command: solver-diagnose
args:
operation: diagnose
@@ -604,7 +618,7 @@ job:
type: assembly
compute:
type: solver
type: custom
command: solver-kinematic
args:
operation: kinematic
@@ -671,7 +685,7 @@ with zipfile.ZipFile("assembly.kc") as zf:
The solver module uses the existing `jobs` table. One new table is added for result caching:
```sql
-- Migration: 020_solver_results.sql
-- Migration: 021_solver_results.sql
CREATE TABLE solver_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -701,7 +715,7 @@ The `UNIQUE(item_id, revision_number, operation)` constraint means each revision
When a solver job completes, the server:
1. Stores the full result in the `jobs.result` JSONB column (standard job result)
2. Upserts a row in `solver_results` for quick lookup by item/revision
3. Broadcasts `solver.completed` SSE event
3. Broadcasts `job.completed` SSE event
---
@@ -840,23 +854,22 @@ Add `to_dict()` / `from_dict()` methods to all KCSolve types in the pybind11 mod
**Verification:** `ctx.to_dict()` round-trips through `SolveContext.from_dict()`.
### Phase 3b: Server Endpoints
### Phase 3b: Server Endpoints -- COMPLETE
Add the solver module to the Silo server.
Add the solver module to the Silo server. This builds on the existing job queue infrastructure (`migration 015_jobs_runners.sql`, `internal/db/jobs.go`, `internal/api/job_handlers.go`, `internal/api/runner_handlers.go`).
**Files to create (in silo repository):**
- `internal/modules/solver/solver.go` -- Module registration and config
- `internal/modules/solver/handlers.go` -- REST endpoint handlers
- `internal/modules/solver/events.go` -- SSE event definitions
- `migrations/020_solver_results.sql` -- Database migration
**Implemented files:**
- `internal/api/solver_handlers.go` -- REST endpoint handlers (solver-specific convenience layer over existing `/api/jobs`)
- `internal/db/migrations/021_solver_results.sql` -- Database migration for result caching table
- Module registered as `solver` in `internal/modules/modules.go` with `jobs` dependency
### Phase 3c: Runner Support
Add solver job execution to `silorunner`.
Add solver command handlers to the existing `silorunner` binary (`cmd/silorunner/main.go`). The runner already implements the full job lifecycle (claim, start, progress, complete/fail). This phase adds `solver-run`, `solver-diagnose`, and `solver-kinematic` to the `executeJob` switch statement.
**Files to create:**
- `src/Mod/Assembly/Solver/bindings/runner.py` -- `kcsolve.runner` entry point
- Runner capability reporting during heartbeat
**Files to modify:**
- `cmd/silorunner/main.go` -- Add solver command dispatch cases
- `src/Mod/Assembly/Solver/bindings/runner.py` -- `kcsolve.runner` Python entry point (invoked by silorunner via subprocess)
### Phase 3d: .kc Context Packing

View File

@@ -1,6 +1,6 @@
# Silo Development Status
**Last Updated:** 2026-02-08
**Last Updated:** 2026-03-01
---
@@ -10,10 +10,10 @@
| Component | Status | Notes |
|-----------|--------|-------|
| PostgreSQL schema | Complete | 18 migrations applied |
| PostgreSQL schema | Complete | 23 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 | 86 REST endpoints via chi/v5 |
| API server (`silod`) | Complete | ~140 REST endpoints via chi/v5 |
| CLI tool (`silo`) | Complete | Item registration and management |
| Filesystem file storage | Complete | Upload, download, checksums |
| Revision control | Complete | Append-only history, rollback, comparison, status/labels |
@@ -35,6 +35,11 @@
| .kc metadata API | Complete | GET/PUT metadata, lifecycle transitions, tag management |
| .kc dependency API | Complete | List raw deps, resolve UUIDs to part numbers + file availability |
| .kc macro API | Complete | List macros, get source content by filename |
| Approval workflows | Complete | YAML-configurable ECO workflows, multi-stage review gates, digital signatures |
| Solver service | Complete | Server-side assembly constraint solving, result caching, job definitions |
| Workstation registration | Complete | Device identity, heartbeat tracking, per-user workstation management |
| Edit sessions | Complete | Acquire/release locks, hard interference detection, SSE notifications |
| SSE targeted delivery | Complete | Per-item, per-user, per-workstation event filtering |
| 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 |
@@ -52,7 +57,7 @@ FreeCAD workbench and LibreOffice Calc extension are maintained in separate repo
| Inventory API endpoints | Database tables exist, no REST handlers |
| Date segment type | Schema parser placeholder only |
| Part number format validation | API accepts but does not validate format on creation |
| Unit tests | 9 Go test files across api, db, ods, partnum, schema packages |
| Unit tests | 31 Go test files across api, db, modules, ods, partnum, schema packages |
---
@@ -106,3 +111,8 @@ The schema defines 170 category codes across 10 groups:
| 016_dag.sql | Dependency DAG nodes and edges |
| 017_locations.sql | Location hierarchy and inventory tracking |
| 018_kc_metadata.sql | .kc metadata tables (item_metadata, item_dependencies, item_macros, item_approvals, approval_signatures) |
| 019_approval_workflow_name.sql | Approval workflow name column |
| 020_storage_backend_filesystem_default.sql | Storage backend default to filesystem |
| 021_solver_results.sql | Solver result caching table |
| 022_workstations.sql | Workstation registration table |
| 023_edit_sessions.sql | Edit session tracking table with hard interference unique index |

View File

@@ -1,7 +1,7 @@
# Worker System Specification
**Status:** Draft
**Last Updated:** 2026-02-13
**Status:** Implemented
**Last Updated:** 2026-03-01
---

View File

@@ -1,757 +0,0 @@
# 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 |
| `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.
**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; // storage 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 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://...", "expires_at": "2026-02-06T..." }
```
The Go handler generates a presigned PUT URL for direct upload. 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 storage. Updates `item.thumbnail_key` column.
### 4. Hierarchical Categories -- IMPLEMENTED (via Form Descriptor)
Resolved by the schema-driven form descriptor (`GET /api/schemas/{name}/form`). The YAML schema's `ui.category_picker` section defines multi-stage selection:
- **Stage 1 (domain)**: Groups categories by first character of category code (F=Fasteners, C=Fluid Fittings, etc.). Values defined in `ui.category_picker.stages[0].values`.
- **Stage 2 (subcategory)**: Auto-derived by the Go backend's `ValuesByDomain()` method, which groups the category enum values by their first character.
No separate `categories` table is needed — the existing schema enum values are the single source of truth. Adding a new category code to the YAML propagates to the picker automatically.
### 5. Database Schema Addition -- IMPLEMENTED
```sql
CREATE TABLE item_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size BIGINT NOT NULL DEFAULT 0,
object_key TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_item_files_item ON item_files(item_id);
ALTER TABLE items ADD COLUMN thumbnail_key TEXT;
ALTER TABLE items ADD COLUMN sourcing_type TEXT NOT NULL DEFAULT 'manufactured';
ALTER TABLE items ADD COLUMN unit_of_measure TEXT NOT NULL DEFAULT 'ea';
ALTER TABLE items ADD COLUMN long_description TEXT;
```
## Implementation Order
1. **[DONE] Deduplicate sourcing_link/standard_cost** — Migrated from item-level DB columns to revision properties (migration 013). Removed from Go structs, API types, frontend types.
2. **[DONE] Form descriptor API** — Added `ui` section to YAML, Go structs + validation, `GET /api/schemas/{name}/form` endpoint.
3. **[DONE] useFormDescriptor hook** — Replaces `useCategories`, fetches and caches form descriptor.
4. **[DONE] CategoryPicker rewrite** — Multi-stage domain/subcategory picker driven by form descriptor.
5. **[DONE] CreateItemPane rewrite** — Dynamic form rendering from field groups, widget-based field rendering.
6. **TagInput component** — reusable, no backend changes needed, uses existing projects API.
7. **FileDropZone + useFileUpload** — requires presigned URL backend endpoint (already implemented).
## Types Added
The following types were added to `web/src/api/types.ts` for the form descriptor system:
```typescript
// Form descriptor types (from GET /api/schemas/{name}/form)
interface FormFieldDescriptor {
name: string;
type: string;
widget: string;
label: string;
required?: boolean;
default?: string;
unit?: string;
description?: string;
options?: string[];
currency?: string;
derived_from_category?: Record<string, string>;
search_endpoint?: string;
}
interface FormFieldGroup {
key: string;
label: string;
order: number;
fields: FormFieldDescriptor[];
}
interface CategoryPickerStage {
name: string;
label: string;
values?: Record<string, string>;
values_by_domain?: Record<string, Record<string, string>>;
}
interface CategoryPickerDescriptor {
style: string;
stages: CategoryPickerStage[];
}
interface ItemFieldDef {
type: string;
widget: string;
label: string;
required?: boolean;
default?: string;
options?: string[];
derived_from_category?: Record<string, string>;
search_endpoint?: string;
}
interface FieldOverride {
widget?: string;
currency?: string;
options?: string[];
}
interface FormDescriptor {
schema_name: string;
format: string;
category_picker: CategoryPickerDescriptor;
item_fields: Record<string, ItemFieldDef>;
field_groups: FormFieldGroup[];
category_field_groups: Record<string, FormFieldGroup[]>;
field_overrides: Record<string, FieldOverride>;
}
// File uploads (unchanged)
interface PresignRequest {
filename: string;
content_type: string;
size: number;
}
interface PresignResponse {
object_key: string;
upload_url: string;
expires_at: string;
}
interface ItemFile {
id: string;
item_id: string;
filename: string;
content_type: string;
size: number;
object_key: string;
created_at: string;
}
// Pending upload (frontend only, not an API type)
interface PendingAttachment {
file: File;
objectKey: string;
uploadProgress: number;
uploadStatus: 'pending' | 'uploading' | 'complete' | 'error';
error?: string;
}
```
Note: `sourcing_link` and `standard_cost` have been removed from the `Item`, `CreateItemRequest`, and `UpdateItemRequest` interfaces — they are now stored as revision properties and rendered dynamically from the form descriptor.

View File

@@ -1,128 +0,0 @@
# 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/ # Filesystem 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, 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 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 |
| [docs/ROADMAP.md](docs/ROADMAP.md) | Platform roadmap, dependency tiers, and gap summary |
| [frontend-spec.md](frontend-spec.md) | React SPA frontend specification |
## License
MIT License - Copyright (c) 2026 Kindred Systems LLC
See [LICENSE](LICENSE) for details.