Compare commits

..

1 Commits

Author SHA1 Message Date
forbes
3b37ff86bd feat(icons): add icon theming infrastructure with Catppuccin color remapping
Some checks failed
Build and Test / build (pull_request) Has been cancelled
- Move kindred-icons/ to icons/kindred/ (better organization)
- Add icons/mappings/ with FCAD.csv (Tango palette) and kindred.csv (Catppuccin Mocha)
- Add icons/retheme.py script to remap upstream FreeCAD SVG colors
- Generate icons/themed/ with 795 auto-themed SVGs (24,698 color replacements)
- Update BitmapFactory.cpp to load icons/kindred (highest priority) then
  icons/themed (auto-remapped upstream icons) before default FreeCAD icons
- Update CMakeLists.txt to install both icon directories
- Update all documentation references to new paths

The 157-color mapping covers the full Tango palette used across FreeCAD's
icon set, interpolating between 4 luminance anchors per color family.
2026-02-15 20:31:00 -06:00
2325 changed files with 10238 additions and 210218 deletions

2
.gitmodules vendored
View File

@@ -13,8 +13,6 @@
[submodule "mods/ztools"]
path = mods/ztools
url = https://git.kindred-systems.com/forbes/ztools.git
branch = main
[submodule "mods/silo"]
path = mods/silo
url = https://git.kindred-systems.com/kindred/silo-mod.git
branch = main

135
README.md

File diff suppressed because one or more lines are too long

View File

@@ -5,116 +5,42 @@
```
FreeCAD startup
└─ src/Mod/Create/Init.py
└─ addon_loader.load_addons(gui=False)
├─ scan_addons("mods/") — find package.xml manifests
parse_manifest() — extract <kindred> extensions
├─ validate_manifest() — check min/max_create_version
├─ resolve_load_order() — topological sort by <dependency>
└─ for each addon in order:
├─ add addon dir to sys.path
├─ exec(Init.py)
└─ register in AddonRegistry (FreeCAD.KindredAddons)
└─ setup_kindred_addons()
├─ exec(mods/ztools/ztools/Init.py)
exec(mods/silo/freecad/Init.py)
└─ src/Mod/Create/InitGui.py
├─ addon_loader.load_addons(gui=True)
for each addon in order:
└─ exec(InitGui.py)
├─ sdk (priority 0): logs "SDK loaded"
├─ ztools (priority 50): schedules deferred _register() (2000ms)
─ imports ZTools commands
│ ├─ installs _ZToolsManipulator (global)
│ └─ injects commands into editing contexts
└─ silo (priority 60): registers SiloWorkbench
│ └─ schedules deferred Silo overlay registration (2500ms)
├─ setup_kindred_workbenches()
exec(mods/ztools/ztools/InitGui.py)
└─ schedules deferred _register() (2000ms)
├─ imports ZTools commands
├─ installs _ZToolsManipulator (global)
─ injects commands into editing contexts
└─ exec(mods/silo/freecad/InitGui.py)
├─ registers SiloWorkbench
└─ schedules deferred Silo overlay registration (2500ms)
├─ EditingContextResolver singleton created (MainWindow constructor)
│ ├─ registers built-in contexts (PartDesign, Sketcher, Assembly, Spreadsheet)
│ ├─ connects to signalInEdit/signalResetEdit/signalActiveDocument/signalActivateView
│ └─ BreadcrumbToolBar connected to contextChanged signal
└─ Deferred setup (QTimer):
├─ 500ms: _register_kc_format() → .kc file format
├─ 1500ms: _register_silo_origin() → registers Silo FileOrigin
├─ 2000ms: _setup_silo_auth_panel() → "Database Auth" dock
├─ 2000ms: ZTools _register() → commands + manipulator
├─ 2500ms: Silo overlay registration → "Silo Origin" toolbar overlay
├─ 3000ms: _check_silo_first_start() → settings prompt
├─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock
├─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock (SSE)
└─ 10000ms: _check_for_updates() → update checker (Gitea API)
```
### Addon lifecycle
Each addon in `mods/` provides a `package.xml` manifest with a `<kindred>` extension block:
```xml
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>50</load_priority>
<pure_python>true</pure_python>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
</kindred>
```
The loader (`addon_loader.py`) processes addons in this order:
1. **Scan** — find all `mods/*/package.xml` files
2. **Parse** — extract `<kindred>` metadata (version bounds, priority, dependencies)
3. **Validate** — reject addons incompatible with the current Create version
4. **Resolve** — topological sort by `<dependency>` declarations, breaking ties by `<load_priority>`
5. **Load** — execute `Init.py` (console) then `InitGui.py` (GUI) for each addon
6. **Register** — populate `FreeCAD.KindredAddons` registry with addon state
Current load order: **sdk** (0) → **ztools** (50) → **silo** (60)
## Key source layout
```
src/Mod/Create/ Kindred Create module
├── Init.py Console bootstrap — loads addons via manifest-driven loader
├── InitGui.py GUI bootstrap — loads addons, Silo integration, update checker
├── addon_loader.py Manifest-driven addon loader with dependency resolution
├── kc_format.py .kc file format round-trip preservation
src/Mod/Create/ Kindred bootstrap module (Python)
├── Init.py Adds mods/ addon paths, loads Init.py files
├── InitGui.py Loads workbenches, installs Silo manipulators
├── version.py.in CMake template → version.py (build-time)
── update_checker.py Checks Gitea releases API for updates
├── CreateGlobal.h C++ export macros (CreateExport, CreateGuiExport)
├── App/ C++ App library (CreateApp.so)
│ ├── AppCreate.cpp Module entry point — PyMOD_INIT_FUNC(CreateApp)
│ └── AppCreatePy.cpp Python module object (Py::ExtensionModule)
└── Gui/ C++ Gui library (CreateGui.so)
├── AppCreateGui.cpp Module entry point — PyMOD_INIT_FUNC(CreateGui)
└── AppCreateGuiPy.cpp Python module object (Py::ExtensionModule)
mods/sdk/ [dir] Kindred addon SDK — stable API contract
├── package.xml Manifest (priority 0, no dependencies)
├── kindred_sdk/
│ ├── __init__.py Public API re-exports
│ ├── context.py Editing context wrappers (register_context, register_overlay, ...)
│ ├── theme.py YAML-driven palette system (get_theme_tokens, load_palette, Palette)
│ ├── origin.py FileOrigin registration (register_origin, unregister_origin)
│ ├── dock.py Deferred dock panel helper (register_dock_panel)
│ ├── compat.py Version detection (create_version, freecad_version)
│ └── palettes/
│ └── catppuccin-mocha.yaml 26 colors + 14 semantic roles
└── Init.py / InitGui.py Minimal log messages
mods/ztools/ [submodule] command provider (not a workbench)
├── package.xml Manifest (priority 50, depends on sdk)
├── ztools/InitGui.py Deferred command registration + _ZToolsManipulator
├── ztools/ztools/
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
│ ├── datums/core.py Datum creation via Part::AttachExtension
│ └── resources/ Icons (SDK theme tokens), theme utilities
└── CatppuccinMocha/ Theme preference pack (QSS)
mods/silo/ [submodule → silo-mod.git] FreeCAD workbench
├── freecad/package.xml Manifest (priority 60, depends on sdk)
├── silo-client/ [submodule → silo-client.git] shared API client
│ └── silo_client/ SiloClient, SiloSettings, CATEGORY_NAMES
└── freecad/ FreeCAD workbench (Python)
├── InitGui.py SiloWorkbench + overlay registration (via SDK)
├── silo_commands.py Commands + FreeCADSiloSettings adapter
└── silo_origin.py FileOrigin backend for Silo (via SDK)
── update_checker.py Checks Gitea releases API for updates
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
@@ -123,6 +49,22 @@ src/Gui/CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
src/Gui/OriginManager.h/.cpp Origin lifecycle management
src/Gui/OriginSelectorWidget.h/.cpp UI for origin selection
mods/ztools/ [submodule] command provider (not a workbench)
├── ztools/InitGui.py Deferred command registration + _ZToolsManipulator
├── ztools/ztools/
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
│ ├── datums/core.py Datum creation via Part::AttachExtension
│ └── resources/ Icons, theme utilities
└── CatppuccinMocha/ Theme preference pack (QSS)
mods/silo/ [submodule -> silo-mod.git] FreeCAD workbench
├── silo-client/ [submodule -> silo-client.git] shared API client
│ └── silo_client/ SiloClient, SiloSettings, CATEGORY_NAMES
└── freecad/ FreeCAD workbench (Python)
├── InitGui.py SiloWorkbench + Silo overlay context registration
├── silo_commands.py Commands + FreeCADSiloSettings adapter
└── silo_origin.py FileOrigin backend for Silo
src/Gui/Stylesheets/ QSS themes and SVG assets
src/Gui/PreferencePacks/ KindredCreate preference pack (cfg + build-time QSS)
```

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
6. **Assembly solver datum handling is minimal.** `UtilsAssembly.findPlacement()` handles standard shapes (faces, edges, vertices) and `App::Line` origin objects. It does not extract placement from `PartDesign::Plane` or `PartDesign::Point` datum objects — when no element is selected, it returns a default `App.Placement()`. This means assembly joints referencing datum planes/points may produce incorrect placement.
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix in `src/Mod/Assembly/UtilsAssembly.py` extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane` and from shape vertex for `PartDesign::Point`. Does not handle empty shapes or non-planar datum objects.
### Medium
@@ -26,9 +26,9 @@
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
10. ~~**`delete_bom_entry()` bypasses error normalization.**~~ Resolved. `delete_bom_entry()` uses `self._request("DELETE", ...)` which routes through `SiloClient._request()` with proper error handling.
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
11. ~~**Missing Silo icons.**~~ Resolved. All three icons now exist: `silo-tag.svg`, `silo-rollback.svg`, `silo-status.svg` in `mods/silo/freecad/resources/icons/`.
11. **Missing Silo icons.** Three commands reference icons that don't exist: `silo-tag.svg` (`Silo_TagProjects`), `silo-rollback.svg` (`Silo_Rollback`), `silo-status.svg` (`Silo_SetStatus`). The `_icon()` function returns an empty string, so these commands render without toolbar icons.
### Fixed (retain for reference)
@@ -50,7 +50,7 @@
| CSRF protection | Implemented | `nosurf` library on web form routes |
| File locking | Not implemented | Needed to prevent concurrent edits |
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
| Part number date segments | Unknown | `formatDate()` reference is stale — function not found in codebase |
| Part number date segments | Broken | `formatDate()` returns error |
| Location/inventory APIs | Tables exist, no handlers | |
| CSV import rollback | Not implemented | `bom_handlers.go` |
| SSE event streaming | Implemented | Reconnect logic with exponential backoff |
@@ -71,20 +71,14 @@
1. **Authentication hardening** -- Deploy FreeIPA and Keycloak infrastructure. End-to-end test LDAP and OIDC flows. Harden token rotation and session expiry.
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save. See `docs/BOM_MERGE.md` for specification.
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
4. ~~**Build system**~~ Done. CMake install rules in `src/Mod/Create/CMakeLists.txt` handle all `mods/` submodules.
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
6. ~~**QSS consolidation**~~ Done. Canonical QSS is `src/Gui/Stylesheets/KindredCreate.qss`; PreferencePacks copy generated at build time via `configure_file()`.
6. **QSS consolidation** -- Eliminate the 3-copy QSS duplication via build-time copy or symlinks. The canonical source is `resources/preferences/KindredCreate/KindredCreate.qss`.
7. **Update notification UI** -- Display in-app notification when a new release is available (issue #30). The update checker backend (`update_checker.py`) runs at startup; notification UI still needed.
8. **KC file format completion** -- Populate `silo_instance` and `revision_hash` in manifest.json. Implement write-back for history.json, approvals.json, dependencies.json. See `docs/KC_SPECIFICATION.md`.
9. **ztools SDK migration** -- Add `<kindred>` metadata to `mods/ztools/package.xml` (load priority, version bounds, SDK dependency). Migrate `InitGui.py` to use `kindred_sdk` APIs for context/overlay registration.
10. **DAG cross-item edges** -- Assembly constraints referencing geometry in child parts should populate `dag_cross_edges`. Deferred until assembly constraint model is finalized.
7. **Update notification UI** -- Display in-app notification when a new release is available (issue #30). The update checker backend is already implemented.

View File

@@ -74,71 +74,46 @@ These files/directories exist only in Kindred Create and can be copied directly
36 Kindred commits touch core FreeCAD C++ files. Of those, **38 files** also changed on `upstream/main`, creating potential conflicts. Listed in chronological order (oldest first) for cherry-pick sequence.
### Category legend
| Category | Meaning | Long-term plan |
|----------|---------|----------------|
| **1 — Extension** | Platform extension points for Python addons. Core differentiator. | KEEP — maintain and isolate |
| **2 — Branding** | Branding and theming. Unavoidable in a fork. | KEEP — minimize surface area |
| **3 — Bug fix** | Bug fixes and polish applicable to upstream FreeCAD. | UPSTREAM — contribute and eliminate |
### Commit sequence
| # | Hash | Summary | Category | Conflict Risk | Files |
|---|------|---------|----------|---------------|-------|
| 1 | `316d4f4b524` | Initial branding (CMakeLists, splash, about, theme, icons, QRC) | 2 — Branding | **HIGH** — CMakeLists.txt, DlgAbout.cpp, SplashScreen.cpp, resource.qrc all changed upstream | 14 files |
| 2 | `8c6837cc152` | Theme padding/clipping fixes | 2 — Branding | LOW — KindredCreate.qss is new file | 1 file |
| 3 | `bb3f3ac6d6c` | Dock task panel right, remove non-Kindred themes | 2 — Branding | **MEDIUM** — MainWindow.cpp, PreferencePacks CMakeLists changed upstream | 8 files |
| 4 | `e85162947b7` | Startup theme selector fix | 2 — Branding | **MEDIUM** — StartupProcess.cpp changed upstream | 4 files |
| 5 | `eb80c07f57a` | Theme QSS refinements | 2 — Branding | LOW — Kindred-only files | 4 files |
| 6 | `8639b6fd8ab` | Resolve unknown command/style token errors | 2 — Branding | **MEDIUM** — StartupProcess.cpp | 5 files |
| 7 | `fea1280fa90` | Theme alternate-background-color | 2 — Branding | LOW | 2 files |
| 8 | `b3fedfb19fb` | Theme tree item padding | 2 — Branding | LOW | 2 files |
| 9 | `0d4545b7d67` | Tree branch line SVGs | 2 — Branding | LOW — new files | 8 files |
| 10 | `434ae797a43` | Theme selector cleanup | 2 — Branding | **MEDIUM** — StartupProcess.cpp | 4 files |
| 11 | `224feda4ad6` | Catppuccin icon override infrastructure | 2 — Branding | **MEDIUM** — BitmapFactory.cpp, CMakeLists.txt | 2 files |
| 12 | `7535a48ec4c` | Origin abstraction layer | 1 — Extension | **HIGH** — Application.cpp, ApplicationPy.cpp/.h, CMakeLists.txt | 6 files (4 new) |
| 13 | `38358e431d2` | LocalFileOrigin and Std_* delegation | 1 — Extension | **HIGH** — CommandDoc.cpp, FileOrigin.cpp/.h | 3 files |
| 14 | `79c85ed2e5d` | FileOriginPython interactive methods | 1 — Extension | LOW — Kindred-only files | 3 files |
| 15 | `deeb6376f71` | OriginSelectorWidget | 1 — Extension | **HIGH** — Action.cpp/.h, CommandStd.cpp, Workbench.cpp, CMakeLists.txt | 8 files |
| 16 | `679aaec6d49` | Origin commands (Commit/Pull/Push) | 1 — Extension | **MEDIUM** — Application.cpp, Command.h, Workbench.cpp | 5 files |
| 17 | `db85277f262` | OriginManagerDialog | 1 — Extension | LOW — new files + OriginSelectorWidget.cpp | 3 files |
| 18 | `015df38328c` | Document-origin tracking in window title | 1 — Extension | **MEDIUM** — MDIView.cpp | 3 files |
| 19 | `a6e84552da5` | Cross-origin detection in SaveAs | 1 — Extension | **MEDIUM** — CommandDoc.cpp | 1 file |
| 20 | `84b69b935b2` | Build fix: remove SelectModule.h include | 1 — Extension | LOW | 1 file |
| 21 | `2f594dac0a5` | Use Python API for viewDefaultOrientation | 1 — Extension | **MEDIUM** — CommandDoc.cpp | 1 file |
| 22 | `724440dcb75` | Build fixes for OriginSelectorWidget/Manager | 1 — Extension | LOW — Kindred files | 2 files |
| 23 | `c858706d480` | Add silo icons to QRC | 2 — Branding | LOW — resource.qrc (additive) | 6 files |
| 24 | `d95c850b7b1` | Widen origin selector widget | 1 — Extension | LOW | 1 file |
| 25 | `10b5c9d584f` | Wire OriginManagerDialog to Silo_Settings | 1 — Extension | LOW | 1 file |
| 26 | `cc5ba638d1f` | UI polish — Wayland scaling, menu icon size | 3 — Bug fix | **HIGH** — DlgSettingsGeneral.cpp/.h/.ui changed upstream | 6 files |
| 27 | `4bf74cf3391` | Build fix for DlgSettingsGeneral | 3 — Bug fix | **MEDIUM** — DlgSettingsGeneral.h, StartupProcess.cpp | 2 files |
| 28 | `6773ca0dfd8` | Eliminate QSS/CFG duplication | 2 — Branding | LOW — Kindred files | 3 files |
| 29 | `977fa3c9347` | QGroupBox indicator, hyperlink color | 2 — Branding | LOW — KindredCreate.qss | 1 file |
| 30 | `8b2ce4b73a4` | Native Qt start panel + kindred:// URL | 1 — Extension | **MEDIUM** — MainWindow.cpp | 1 file |
| 31 | `bf637af4e45` | Window flickering and icon clipping fix | 3 — Bug fix | **MEDIUM** — MainWindow.cpp/.h | 3 files |
| 32 | `70118201b02` | MDI pre-document tab for Silo new item | 1 — Extension | **HIGH** — ApplicationPy, BreadcrumbToolBar, EditingContext, MainWindow, Workbench | 11 files |
| 33 | `1f49e3fa6da` | Build fix: ToolBarItem incomplete type, reportException | 3 — Bug fix | **LOW** — Kindred-specific build fix | 2 files |
| 34 | `f71decca089` | Splash screen: skip runtime draw, mantle bg | 2 — Branding | **MEDIUM** — SplashScreen.cpp | 3 files |
| 35 | `2b0c6774c07` | Dev build defaults, skip version migration | 2 — Branding | **MEDIUM** — CMakeLists.txt, DlgVersionMigrator.cpp | 2 files |
| 36 | `ab71902a4c2` | Forward visibility arg in appendToolbar() | 1 — Extension | LOW — FreeCADGuiInit.py | 1 file |
### Category summary
| Category | Count | Commits |
|----------|-------|---------|
| 1 — Extension | 16 | #1222, #2425, #30, #32, #36 |
| 2 — Branding | 16 | #111, #23, #2829, #3435 |
| 3 — Bug fix | 4 | #2627, #31, #33 |
### Category 3 — Upstream status (verified Feb 2026)
| # | Summary | Upstream Fixed? | FreeCAD Issues | Notes |
|---|---------|-----------------|----------------|-------|
| 26 | Wayland scaling, menu icon size pref | PARTIAL | [#25448](https://github.com/FreeCAD/FreeCAD/issues/25448), [#23830](https://github.com/FreeCAD/FreeCAD/issues/23830), [#23396](https://github.com/FreeCAD/FreeCAD/issues/23396) | Upstream has some HiDPI C++ fixes but Wayland fractional scaling still open. Menu icon size pref is a Kindred-added feature, not yet contributed. |
| 27 | Build fix for DlgSettingsGeneral | NO | — | Companion to #26. `setMenuIconSize()` in StartupProcess is Kindred-added. |
| 31 | Window flickering and icon clipping | NO | [#12917](https://github.com/FreeCAD/FreeCAD/issues/12917), [#18481](https://github.com/FreeCAD/FreeCAD/issues/18481) | Upstream still uses `setStyleSheet()` on statusBar causing repaint cascade. Good upstream PR candidate — replace with `setPalette()`. |
| 33 | ToolBarItem incomplete type, reportException | N/A | — | Kindred-internal build fix. The `ToolBarItem` forward-decl issue only manifests with Kindred modifications to Workbench.h. The `reportException` typo is in Kindred-only editing context code. |
| # | Hash | Summary | Conflict Risk | Files |
|---|------|---------|---------------|-------|
| 1 | `316d4f4b524` | Initial branding (CMakeLists, splash, about, theme, icons, QRC) | **HIGH** — CMakeLists.txt, DlgAbout.cpp, SplashScreen.cpp, resource.qrc all changed upstream | 14 files |
| 2 | `8c6837cc152` | Theme padding/clipping fixes | LOW — KindredCreate.qss is new file | 1 file |
| 3 | `bb3f3ac6d6c` | Dock task panel right, remove non-Kindred themes | **MEDIUM** — MainWindow.cpp, PreferencePacks CMakeLists changed upstream | 8 files |
| 4 | `e85162947b7` | Startup theme selector fix | **MEDIUM** — StartupProcess.cpp changed upstream | 4 files |
| 5 | `eb80c07f57a` | Theme QSS refinements | LOW — Kindred-only files | 4 files |
| 6 | `8639b6fd8ab` | Resolve unknown command/style token errors | **MEDIUM** — StartupProcess.cpp | 5 files |
| 7 | `fea1280fa90` | Theme alternate-background-color | LOW | 2 files |
| 8 | `b3fedfb19fb` | Theme tree item padding | LOW | 2 files |
| 9 | `0d4545b7d67` | Tree branch line SVGs | LOW — new files | 8 files |
| 10 | `434ae797a43` | Theme selector cleanup | **MEDIUM** — StartupProcess.cpp | 4 files |
| 11 | `224feda4ad6` | Catppuccin icon override infrastructure | **MEDIUM** — BitmapFactory.cpp, CMakeLists.txt | 2 files |
| 12 | `7535a48ec4c` | Origin abstraction layer | **HIGH** — Application.cpp, ApplicationPy.cpp/.h, CMakeLists.txt | 6 files (4 new) |
| 13 | `38358e431d2` | LocalFileOrigin and Std_* delegation | **HIGH** — CommandDoc.cpp, FileOrigin.cpp/.h | 3 files |
| 14 | `79c85ed2e5d` | FileOriginPython interactive methods | LOW — Kindred-only files | 3 files |
| 15 | `deeb6376f71` | OriginSelectorWidget | **HIGH** — Action.cpp/.h, CommandStd.cpp, Workbench.cpp, CMakeLists.txt | 8 files |
| 16 | `679aaec6d49` | Origin commands (Commit/Pull/Push) | **MEDIUM** — Application.cpp, Command.h, Workbench.cpp | 5 files |
| 17 | `db85277f262` | OriginManagerDialog | LOW — new files + OriginSelectorWidget.cpp | 3 files |
| 18 | `015df38328c` | Document-origin tracking in window title | **MEDIUM** — MDIView.cpp | 3 files |
| 19 | `a6e84552da5` | Cross-origin detection in SaveAs | **MEDIUM** — CommandDoc.cpp | 1 file |
| 20 | `84b69b935b2` | Build fix: remove SelectModule.h include | LOW | 1 file |
| 21 | `2f594dac0a5` | Use Python API for viewDefaultOrientation | **MEDIUM** — CommandDoc.cpp | 1 file |
| 22 | `724440dcb75` | Build fixes for OriginSelectorWidget/Manager | LOW — Kindred files | 2 files |
| 23 | `c858706d480` | Add silo icons to QRC | LOW — resource.qrc (additive) | 6 files |
| 24 | `d95c850b7b1` | Widen origin selector widget | LOW | 1 file |
| 25 | `10b5c9d584f` | Wire OriginManagerDialog to Silo_Settings | LOW | 1 file |
| 26 | `cc5ba638d1f` | UI polish — Wayland scaling, menu icon size | **HIGH** — DlgSettingsGeneral.cpp/.h/.ui changed upstream | 6 files |
| 27 | `4bf74cf3391` | Build fix for DlgSettingsGeneral | **MEDIUM** — DlgSettingsGeneral.h, StartupProcess.cpp | 2 files |
| 28 | `6773ca0dfd8` | Eliminate QSS/CFG duplication | LOW — Kindred files | 3 files |
| 29 | `977fa3c9347` | QGroupBox indicator, hyperlink color | LOW — KindredCreate.qss | 1 file |
| 30 | `8b2ce4b73a4` | Native Qt start panel + kindred:// URL | **MEDIUM** — MainWindow.cpp | 1 file |
| 31 | `bf637af4e45` | Window flickering and icon clipping fix | **MEDIUM** — MainWindow.cpp/.h | 3 files |
| 32 | `70118201b02` | MDI pre-document tab for Silo new item | **HIGH** — ApplicationPy, BreadcrumbToolBar, EditingContext, MainWindow, Workbench | 11 files |
| 33 | `1f49e3fa6da` | Build fix: ToolBarItem incomplete type, reportException | **LOW** — likely already fixed upstream | 2 files |
| 34 | `f71decca089` | Splash screen: skip runtime draw, mantle bg | **MEDIUM** — SplashScreen.cpp | 3 files |
| 35 | `2b0c6774c07` | Dev build defaults, skip version migration | **MEDIUM** — CMakeLists.txt, DlgVersionMigrator.cpp | 2 files |
| 36 | `ab71902a4c2` | Forward visibility arg in appendToolbar() | LOW — FreeCADGuiInit.py | 1 file |
### HIGH conflict files (need manual attention)
@@ -162,14 +137,14 @@ These files are modified by both Kindred and upstream. They will almost certainl
## Phase 3: Module-Level Changes
### Assembly fixes (verified Feb 2026)
### Assembly fixes (check if still needed)
| Hash | Summary | Upstream Status | Action |
|------|---------|-----------------|--------|
| `316d4f4b524` | Joint flip overconstrain fix (91° rotation guard) | **NOT fixed** — upstream has basic grounded-object validation only. [FreeCAD#20377](https://github.com/FreeCAD/FreeCAD/issues/20377) open. | KEEP — re-apply |
| `9dc50cef727` | SIGSEGV during document restore (`isRestoring()` guard) | **NOT fixed** — upstream `onChanged()` has no restore guard. [FreeCAD#18225](https://github.com/FreeCAD/FreeCAD/issues/18225) closed via different path. | KEEP — re-apply |
| `ddefb236521` | Solver ignoring datum plane refs (`PartDesign::Plane`/`Point`) | **NOT fixed** — upstream `findPlacement()` lacks these types. | KEEP — re-apply, upstream candidate |
| `b7374d7b1fc` | findPlacement() datum/origin handling (`App::Plane`/`Point`) | **PARTIAL**`App::Line` fixed upstream via [PR#20026](https://github.com/FreeCAD/FreeCAD/pull/20026). `App::Plane`/`App::Point` still missing. | KEEP — re-apply the `Plane`/`Point` parts only |
| Hash | Summary | Status |
|------|---------|--------|
| `316d4f4b524` | Joint flip overconstrain fix | May be fixed upstream — verify |
| `9dc50cef727` | SIGSEGV during document restore | May be fixed upstream — verify |
| `ddefb236521` | Solver ignoring datum plane refs | May be fixed upstream — verify |
| `b7374d7b1fc` | findPlacement() datum/origin handling | May be fixed upstream — verify |
Files touched: `src/Mod/Assembly/App/AssemblyObject.cpp`, `src/Mod/Assembly/UtilsAssembly.py`, `src/Mod/Assembly/InitGui.py`

View File

@@ -28,7 +28,6 @@
- [Repository Structure](./development/repo-structure.md)
- [Build System](./development/build-system.md)
- [Gui Module Build](./development/gui-build-integration.md)
- [Package.xml Schema Extensions](./development/package-xml-schema.md)
# Silo Server

View File

@@ -1,109 +0,0 @@
# Package.xml Schema Extensions
Kindred Create extends FreeCAD's standard `package.xml` addon manifest with a `<kindred>` element. This element provides metadata for the manifest-driven addon loader: version compatibility, load ordering, dependency declarations, and editing context registration.
The `<kindred>` element is ignored by FreeCAD's AddonManager and stock module loader. Addons with this element remain compatible with upstream FreeCAD.
## Field reference
| Field | Parsed by loader | Required | Default | Description |
|---|---|---|---|---|
| `min_create_version` | Yes | No | *(none)* | Minimum Kindred Create version. Addon is skipped if the running version is lower. |
| `max_create_version` | Yes | No | *(none)* | Maximum Kindred Create version. Addon is skipped if the running version is higher. |
| `load_priority` | Yes | No | `100` | Integer. Lower values load first. Used as a secondary sort after dependency resolution. |
| `dependencies` | Yes | No | *(none)* | List of addon names (by `<name>` in their `package.xml`) that must load before this one. |
| `sdk_version` | No | No | *(none)* | Required kindred-addon-sdk version. Reserved for future use when the SDK is available. |
| `pure_python` | No | No | `true` | If `false`, the addon requires compiled C++ components. Informational. |
| `contexts` | No | No | *(none)* | Editing contexts this addon registers or injects into. Informational. |
Fields marked "Parsed by loader" are read by `addon_loader.py` and affect load behavior. Other fields are informational metadata for tooling and documentation.
## Schema
```xml
<kindred>
<min_create_version>0.1.0</min_create_version>
<max_create_version>1.0.0</max_create_version>
<sdk_version>0.1.0</sdk_version>
<load_priority>100</load_priority>
<pure_python>true</pure_python>
<dependencies>
<dependency>sdk</dependency>
<dependency>other-addon</dependency>
</dependencies>
<contexts>
<context id="partdesign.body" action="inject"/>
<context id="sketcher.edit" action="register"/>
<context id="*" action="overlay"/>
</contexts>
</kindred>
```
All child elements are optional. An empty `<kindred/>` element is valid and signals that the addon is Kindred-aware with all defaults.
### Version fields
`min_create_version` and `max_create_version` are compared against the running Kindred Create version using semantic versioning (major.minor.patch). If the running version falls outside the declared range, the addon is skipped with a warning in the report view.
### Load priority
When multiple addons have no dependency relationship, `load_priority` determines their relative order. Lower values load first. Suggested ranges:
| Range | Use |
|---|---|
| 0-9 | SDK and core infrastructure |
| 10-49 | Foundation addons that others may depend on |
| 50-99 | Standard addons |
| 100+ | Optional or late-loading addons |
### Dependencies
Each `<dependency>` names another addon by its `<name>` element in `package.xml`. The loader resolves load order using topological sort. If a dependency is not found among discovered addons, the dependent addon is skipped.
### Contexts
The `<contexts>` element documents which editing contexts the addon interacts with. The `action` attribute describes the type of interaction:
| Action | Meaning |
|---|---|
| `inject` | Addon injects commands into this context's toolbars |
| `register` | Addon registers this as a new context |
| `overlay` | Addon registers an overlay that may apply across contexts |
A wildcard `id="*"` indicates the addon's overlay applies universally (matched by a condition function rather than a specific context ID).
## Example: complete package.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>MyAddon</name>
<description>Example Kindred Create addon</description>
<version>0.2.0</version>
<maintainer email="dev@example.com">Developer</maintainer>
<license>LGPL-2.1-or-later</license>
<url type="repository">https://git.example.com/myaddon</url>
<content>
<workbench>
<classname>MyAddonWorkbench</classname>
<subdirectory>./</subdirectory>
</workbench>
</content>
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>80</load_priority>
<pure_python>true</pure_python>
<contexts>
<context id="partdesign.body" action="inject"/>
</contexts>
</kindred>
</package>
```
## Backward compatibility
- The `<kindred>` element sits outside `<content>` and is not part of FreeCAD's package.xml specification. FreeCAD's `App.Metadata` C++ parser and the AddonManager's Python `MetadataReader` both ignore unknown elements.
- An addon installed in stock FreeCAD will work normally; the `<kindred>` extensions are simply unused.
- The Kindred Create loader works with or without the `<kindred>` element. Addons that omit it load with no version constraints and default priority (100).

View File

@@ -73,27 +73,25 @@ database:
---
## Storage (Filesystem)
## Storage (MinIO/S3)
Files are stored on the local filesystem under a configurable root directory.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `storage.backend` | string | `"filesystem"` | Storage backend (`filesystem`) |
| `storage.filesystem.root_dir` | string | — | Root directory for file storage (required) |
| Key | Type | Default | Env Override | Description |
|-----|------|---------|-------------|-------------|
| `storage.endpoint` | string | — | `SILO_MINIO_ENDPOINT` | MinIO/S3 endpoint (`host:port`) |
| `storage.access_key` | string | — | `SILO_MINIO_ACCESS_KEY` | Access key |
| `storage.secret_key` | string | — | `SILO_MINIO_SECRET_KEY` | Secret key |
| `storage.bucket` | string | — | — | S3 bucket name (created automatically if missing) |
| `storage.use_ssl` | bool | `false` | — | Use HTTPS for MinIO connections |
| `storage.region` | string | `"us-east-1"` | — | S3 region |
```yaml
storage:
backend: "filesystem"
filesystem:
root_dir: "/opt/silo/data"
```
Ensure the directory exists and is writable by the `silo` user:
```bash
sudo mkdir -p /opt/silo/data
sudo chown silo:silo /opt/silo/data
endpoint: "localhost:9000"
access_key: "" # use SILO_MINIO_ACCESS_KEY env var
secret_key: "" # use SILO_MINIO_SECRET_KEY env var
bucket: "silo-files"
use_ssl: false
region: "us-east-1"
```
---
@@ -266,6 +264,9 @@ All environment variable overrides. These take precedence over values in `config
| `SILO_DB_NAME` | `database.name` | PostgreSQL database name |
| `SILO_DB_USER` | `database.user` | PostgreSQL user |
| `SILO_DB_PASSWORD` | `database.password` | PostgreSQL password |
| `SILO_MINIO_ENDPOINT` | `storage.endpoint` | MinIO endpoint |
| `SILO_MINIO_ACCESS_KEY` | `storage.access_key` | MinIO access key |
| `SILO_MINIO_SECRET_KEY` | `storage.secret_key` | MinIO secret key |
| `SILO_SESSION_SECRET` | `auth.session_secret` | Session cookie signing secret |
| `SILO_ADMIN_USERNAME` | `auth.local.default_admin_username` | Default admin username |
| `SILO_ADMIN_PASSWORD` | `auth.local.default_admin_password` | Default admin password |
@@ -295,9 +296,11 @@ database:
sslmode: "disable"
storage:
backend: "filesystem"
filesystem:
root_dir: "./data"
endpoint: "localhost:9000"
access_key: "minioadmin"
secret_key: "minioadmin"
bucket: "silo-files"
use_ssl: false
schemas:
directory: "./schemas"

View File

@@ -4,7 +4,7 @@
> instructions. This document covers ongoing maintenance and operations for an
> existing deployment.
This guide covers deploying Silo to a dedicated VM using external PostgreSQL and local filesystem storage.
This guide covers deploying Silo to a dedicated VM using external PostgreSQL and MinIO services.
## Table of Contents
@@ -26,25 +26,28 @@ This guide covers deploying Silo to a dedicated VM using external PostgreSQL and
│ │ silod │ │
│ │ (Silo API Server) │ │
│ │ :8080 │ │
│ │ Files: /opt/silo/data │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────┐
│ psql.example.internal │
│ PostgreSQL 16 │
│ :5432 │
└─────────────────────────┘
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ psql.example.internal │ │ minio.example.internal │
│ PostgreSQL 16 │ │ MinIO S3 │
│ :5432 │ │ :9000 (API) │
│ │ │ :9001 (Console) │
└─────────────────────────┘ └─────────────────────────────────┘
```
## External Services
| Service | Host | Database | User |
|---------|------|----------|------|
| PostgreSQL | psql.example.internal:5432 | silo | silo |
The following external services are already configured:
Files are stored on the local filesystem at `/opt/silo/data`. Migrations have been applied to the database.
| Service | Host | Database/Bucket | User |
|---------|------|-----------------|------|
| PostgreSQL | psql.example.internal:5432 | silo | silo |
| MinIO | minio.example.internal:9000 | silo-files | silouser |
Migrations have been applied to the database.
---
@@ -104,15 +107,21 @@ Fill in the values:
# Database credentials (psql.example.internal)
SILO_DB_PASSWORD=your-database-password
# MinIO credentials (minio.example.internal)
SILO_MINIO_ACCESS_KEY=silouser
SILO_MINIO_SECRET_KEY=your-minio-secret-key
```
### Verify External Services
Before deploying, verify connectivity to PostgreSQL:
Before deploying, verify connectivity to external services:
```bash
# Test PostgreSQL
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
# Test MinIO
curl -I http://minio.example.internal:9000/minio/health/live
```
---
@@ -174,7 +183,6 @@ sudo -E /opt/silo/src/scripts/deploy.sh
| File | Purpose |
|------|---------|
| `/opt/silo/bin/silod` | Server binary |
| `/opt/silo/data/` | File storage root |
| `/opt/silo/src/` | Git repository checkout |
| `/etc/silo/config.yaml` | Server configuration |
| `/etc/silo/silod.env` | Environment variables (secrets) |
@@ -234,7 +242,7 @@ sudo journalctl -u silod --since "2024-01-15 10:00:00"
# Basic health check
curl http://localhost:8080/health
# Full readiness check (includes DB)
# Full readiness check (includes DB and MinIO)
curl http://localhost:8080/ready
```
@@ -310,6 +318,24 @@ psql -h psql.example.internal -U silo -d silo -f /opt/silo/src/migrations/008_ne
3. Check `pg_hba.conf` on PostgreSQL server allows connections from this host.
### Connection Refused to MinIO
1. Test network connectivity:
```bash
nc -zv minio.example.internal 9000
```
2. Test with curl:
```bash
curl -I http://minio.example.internal:9000/minio/health/live
```
3. Check SSL settings in config match MinIO setup:
```yaml
storage:
use_ssl: true # or false
```
### Health Check Fails
```bash
@@ -319,9 +345,7 @@ curl -v http://localhost:8080/ready
# If ready fails but health passes, check external services
psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'
# Check file storage directory
ls -la /opt/silo/data
curl http://minio.example.internal:9000/minio/health/live
```
### Build Fails
@@ -436,9 +460,10 @@ sudo systemctl reload nginx
- [ ] `/etc/silo/silod.env` has mode 600 (`chmod 600`)
- [ ] Database password is strong and unique
- [ ] MinIO credentials are specific to silo (not admin)
- [ ] SSL/TLS enabled for PostgreSQL (`sslmode: require`)
- [ ] SSL/TLS enabled for MinIO (`use_ssl: true`) if available
- [ ] HTTPS enabled via nginx reverse proxy
- [ ] File storage directory (`/opt/silo/data`) owned by `silo` user with mode 750
- [ ] Silod listens on localhost only (`host: 127.0.0.1`)
- [ ] Firewall allows only ports 80, 443 (not 8080)
- [ ] Service runs as non-root `silo` user

View File

@@ -76,7 +76,7 @@ See [ROADMAP.md](ROADMAP.md) for the platform roadmap and dependency tier struct
| Append-only revision history | Complete | `internal/db/items.go` |
| Sequential revision numbering | Complete | Database trigger |
| Property snapshots (JSONB) | Complete | `revisions.properties` |
| File storage (filesystem) | Complete | `internal/storage/` |
| File versioning (MinIO) | Complete | `internal/storage/` |
| SHA256 checksums | Complete | Captured on upload |
| Revision comments | Complete | `revisions.comment` |
| User attribution | Complete | `revisions.created_by` |
@@ -93,7 +93,7 @@ CREATE TABLE revisions (
revision_number INTEGER NOT NULL,
properties JSONB NOT NULL DEFAULT '{}',
file_key TEXT,
file_version TEXT, -- storage version ID
file_version TEXT, -- MinIO version ID
file_checksum TEXT, -- SHA256
file_size BIGINT,
thumbnail_key TEXT,
@@ -283,7 +283,7 @@ Effort: Medium | Priority: Low | Risk: Low
**Changes:**
- Add thumbnail generation on file upload
- Store at `thumbnails/{part_number}/rev{n}.png`
- Store in MinIO at `thumbnails/{part_number}/rev{n}.png`
- Expose via `GET /api/items/{pn}/thumbnail/{rev}`
---
@@ -377,7 +377,7 @@ internal/
relationships.go # BOM repository
projects.go # Project repository
storage/
storage.go # File storage helpers
storage.go # MinIO file storage helpers
migrations/
001_initial.sql # Core schema
...
@@ -572,7 +572,7 @@ Reporting capabilities are absent. Basic reports (item counts, revision activity
| Feature | SOLIDWORKS PDM | Silo Status | Priority | Complexity |
|---------|---------------|-------------|----------|------------|
| File versioning | Automatic | Full (filesystem) | - | - |
| File versioning | Automatic | Full (MinIO) | - | - |
| File preview | Thumbnails, 3D preview | None | Medium | Complex |
| File conversion | PDF, DXF generation | None | Medium | Complex |
| Replication | Multi-site sync | None | Low | Complex |

View File

@@ -3,7 +3,7 @@
This guide covers two installation methods:
- **[Option A: Docker Compose](#option-a-docker-compose)** — self-contained stack with all services. Recommended for evaluation, small teams, and environments where Docker is the standard.
- **[Option B: Daemon Install](#option-b-daemon-install-systemd--external-services)** — systemd service with external PostgreSQL and optional LDAP/nginx. Files are stored on the local filesystem. Recommended for production deployments integrated with existing infrastructure.
- **[Option B: Daemon Install](#option-b-daemon-install-systemd--external-services)** — systemd service with external PostgreSQL, MinIO, and optional LDAP/nginx. Recommended for production deployments integrated with existing infrastructure.
Both methods produce the same result: a running Silo server with a web UI, REST API, and authentication.
@@ -48,7 +48,7 @@ Regardless of which method you choose:
## Option A: Docker Compose
A single Docker Compose file runs everything: PostgreSQL, OpenLDAP, and Silo. Files are stored on the local filesystem. An optional nginx container can be enabled for reverse proxying.
A single Docker Compose file runs everything: PostgreSQL, MinIO, OpenLDAP, and Silo. An optional nginx container can be enabled for reverse proxying.
### A.1 Prerequisites
@@ -80,6 +80,7 @@ The setup script generates credentials and configuration files:
It prompts for:
- Server domain (default: `localhost`)
- PostgreSQL password (auto-generated if you press Enter)
- MinIO credentials (auto-generated)
- OpenLDAP admin password and initial user (auto-generated)
- Silo local admin account (fallback when LDAP is unavailable)
@@ -105,7 +106,7 @@ Wait for all services to become healthy:
docker compose -f deployments/docker-compose.allinone.yaml ps
```
You should see `silo-postgres`, `silo-openldap`, and `silo-api` all in a healthy state.
You should see `silo-postgres`, `silo-minio`, `silo-openldap`, and `silo-api` all in a healthy state.
View logs:
@@ -123,7 +124,7 @@ docker compose -f deployments/docker-compose.allinone.yaml logs -f silo
# Health check
curl http://localhost:8080/health
# Readiness check (includes database connectivity)
# Readiness check (includes database and storage connectivity)
curl http://localhost:8080/ready
```
@@ -225,7 +226,7 @@ The Silo container is rebuilt from the updated source. Database migrations in `m
## Option B: Daemon Install (systemd + External Services)
This method runs Silo as a systemd service on a dedicated host, connecting to externally managed PostgreSQL and optionally LDAP services. Files are stored on the local filesystem.
This method runs Silo as a systemd service on a dedicated host, connecting to externally managed PostgreSQL, MinIO, and optionally LDAP services.
### B.1 Architecture Overview
@@ -239,22 +240,21 @@ This method runs Silo as a systemd service on a dedicated host, connecting to ex
│ ┌───────▼────────┐ │
│ │ silod │ │
│ │ (API server) │ │
│ Files: /opt/ │
│ │ silo/data │ │
│ └──────┬─────────┘
─────────────────────
┌───────────▼──┐
│ PostgreSQL 16
:5432
└──────────────┘
└──┬─────────┬───┘
└─────┼─────────┼──────┘
┌─────────────┐ ┌───▼──────────────┐
│ PostgreSQL 16│ │ MinIO (S3)
:5432 │ │ :9000 API │
└──────────────┘ │ :9001 Console
└──────────────────┘
```
### B.2 Prerequisites
- Linux host (Debian/Ubuntu or RHEL/Fedora/AlmaLinux)
- Root or sudo access
- Network access to your PostgreSQL server
- Network access to your PostgreSQL and MinIO servers
The setup script installs Go and other build dependencies automatically.
@@ -281,6 +281,26 @@ Verify:
psql -h YOUR_PG_HOST -U silo -d silo -c 'SELECT 1'
```
#### MinIO
Install MinIO and create a bucket and service account:
- [MinIO quickstart](https://min.io/docs/minio/linux/index.html)
```bash
# Using the MinIO client (mc):
mc alias set local http://YOUR_MINIO_HOST:9000 minioadmin minioadmin
mc mb local/silo-files
mc admin user add local silouser YOUR_MINIO_SECRET
mc admin policy attach local readwrite --user silouser
```
Verify:
```bash
curl -I http://YOUR_MINIO_HOST:9000/minio/health/live
```
#### LDAP / FreeIPA (Optional)
For LDAP authentication, you need an LDAP server with user and group entries. Options:
@@ -319,10 +339,10 @@ The script:
4. Clones the repository
5. Creates the environment file template
To override the default database hostname:
To override the default service hostnames:
```bash
SILO_DB_HOST=db.example.com sudo -E bash scripts/setup-host.sh
SILO_DB_HOST=db.example.com SILO_MINIO_HOST=s3.example.com sudo -E bash scripts/setup-host.sh
```
### B.5 Configure Credentials
@@ -337,6 +357,10 @@ sudo nano /etc/silo/silod.env
# Database
SILO_DB_PASSWORD=your-database-password
# MinIO
SILO_MINIO_ACCESS_KEY=silouser
SILO_MINIO_SECRET_KEY=your-minio-secret
# Authentication
SILO_SESSION_SECRET=generate-a-long-random-string
SILO_ADMIN_USERNAME=admin
@@ -355,7 +379,7 @@ Review the server configuration:
sudo nano /etc/silo/config.yaml
```
Update `database.host`, `storage.filesystem.root_dir`, `server.base_url`, and authentication settings for your environment. See [CONFIGURATION.md](CONFIGURATION.md) for all options.
Update `database.host`, `storage.endpoint`, `server.base_url`, and authentication settings for your environment. See [CONFIGURATION.md](CONFIGURATION.md) for all options.
### B.6 Deploy
@@ -388,10 +412,10 @@ sudo /opt/silo/src/scripts/deploy.sh --restart-only
sudo /opt/silo/src/scripts/deploy.sh --status
```
To override the target host:
To override the target host or database host:
```bash
SILO_DEPLOY_TARGET=silo.example.com sudo -E scripts/deploy.sh
SILO_DEPLOY_TARGET=silo.example.com SILO_DB_HOST=db.example.com sudo -E scripts/deploy.sh
```
### B.7 Set Up Nginx and TLS

View File

@@ -1,485 +0,0 @@
# .kc Server-Side Metadata Integration
**Status:** Draft
**Date:** February 2026
---
## 1. Purpose
When a `.kc` file is committed to Silo, the server extracts and indexes the `silo/` directory contents so that metadata is queryable, diffable, and streamable without downloading the full file. This document specifies the server-side processing pipeline, database storage, API endpoints, and SSE events that support the Create viewport widgets defined in [SILO_VIEWPORT.md](SILO_VIEWPORT.md).
The core principle: **the `.kc` file is the transport format; Silo is the index.** The `silo/` directory entries are extracted into database columns on commit and packed back into the ZIP on checkout. The server never modifies the FreeCAD standard zone (`Document.xml`, `.brp` files, `thumbnails/`).
---
## 2. Commit Pipeline
When a `.kc` file is uploaded via `POST /api/items/{partNumber}/file`, the server runs an extraction pipeline before returning success.
### 2.1 Pipeline Steps
```
Client uploads .kc file
|
v
+-----------------------------+
| 1. Store file to disk | (existing behavior -- unchanged)
| items/{pn}/rev{N}.kc |
+-----------------------------+
|
v
+-----------------------------+
| 2. Open ZIP, read silo/ |
| Parse each entry |
+-----------------------------+
|
v
+-----------------------------+
| 3. Validate manifest.json |
| - UUID matches item |
| - kc_version supported |
| - revision_hash present |
+-----------------------------+
|
v
+-----------------------------+
| 4. Index metadata |
| - Upsert item_metadata |
| - Upsert dependencies |
| - Append history entry |
| - Snapshot approvals |
| - Register macros |
| - Register job defs |
+-----------------------------+
|
v
+-----------------------------+
| 5. Broadcast SSE events |
| - revision.created |
| - metadata.updated |
| - bom.changed (if deps |
| differ from previous) |
+-----------------------------+
|
v
Return 201 Created
```
### 2.2 Validation Rules
| Check | Failure response |
|-------|-----------------|
| `silo/manifest.json` missing | `400 Bad Request` -- file is `.fcstd` not `.kc` |
| `manifest.uuid` doesn't match item's UUID | `409 Conflict` -- wrong item |
| `manifest.kc_version` > server's supported version | `422 Unprocessable` -- client newer than server |
| `manifest.revision_hash` matches current head | `200 OK` (no-op, file unchanged) |
| Any `silo/` JSON fails to parse | `422 Unprocessable` with path and parse error |
If validation fails, the blob is still stored (the user uploaded it), but no metadata indexing occurs. The item's revision is created with a `metadata_error` flag so the web UI can surface the problem.
### 2.3 Backward Compatibility
Plain `.fcstd` files (no `silo/` directory) continue to work exactly as today -- stored on disk, revision created, no metadata extraction. The pipeline short-circuits at step 2 when no `silo/` directory is found.
---
## 3. Database Schema
### 3.1 `item_metadata` Table
Stores the indexed contents of `silo/metadata.json` as structured JSONB, searchable and filterable via the existing item query endpoints.
```sql
CREATE TABLE item_metadata (
item_id UUID PRIMARY KEY REFERENCES items(id) ON DELETE CASCADE,
schema_name TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
lifecycle_state TEXT NOT NULL DEFAULT 'draft',
fields JSONB NOT NULL DEFAULT '{}',
kc_version TEXT,
manifest_uuid UUID,
silo_instance TEXT,
revision_hash TEXT,
updated_at TIMESTAMPTZ DEFAULT now(),
updated_by TEXT
);
CREATE INDEX idx_item_metadata_tags ON item_metadata USING GIN (tags);
CREATE INDEX idx_item_metadata_lifecycle ON item_metadata (lifecycle_state);
CREATE INDEX idx_item_metadata_fields ON item_metadata USING GIN (fields);
```
On commit, the server upserts this row from `silo/manifest.json` and `silo/metadata.json`. The `fields` column contains the schema-driven key-value pairs exactly as they appear in the JSON.
### 3.2 `item_dependencies` Table
Stores the indexed contents of `silo/dependencies.json`. Replaces the BOM for assembly relationships that originate from the CAD model.
```sql
CREATE TABLE item_dependencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_item_id UUID REFERENCES items(id) ON DELETE CASCADE,
child_uuid UUID NOT NULL,
child_part_number TEXT,
child_revision INTEGER,
quantity DECIMAL,
label TEXT,
relationship TEXT NOT NULL DEFAULT 'component',
revision_number INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_item_deps_parent ON item_dependencies (parent_item_id);
CREATE INDEX idx_item_deps_child ON item_dependencies (child_uuid);
```
This table complements the existing `relationships` table. The `relationships` table is the server-authoritative BOM (editable via the web UI and API). The `item_dependencies` table is the CAD-authoritative record extracted from the file. BOM merge (per [BOM_MERGE.md](BOM_MERGE.md)) reconciles the two.
### 3.3 `item_approvals` Table
Stores the indexed contents of `silo/approvals.json`. Server-authoritative -- the `.kc` snapshot is a read cache.
```sql
CREATE TABLE item_approvals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID REFERENCES items(id) ON DELETE CASCADE,
eco_number TEXT,
state TEXT NOT NULL DEFAULT 'draft',
updated_at TIMESTAMPTZ DEFAULT now(),
updated_by TEXT
);
CREATE TABLE approval_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
approval_id UUID 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
);
```
These tables exist independent of `.kc` commits -- approvals are created and managed through the web UI and API. On `.kc` checkout, the current approval state is serialized into `silo/approvals.json` for offline display.
### 3.4 `item_macros` Table
Registers macros from `silo/macros/` for server-side discoverability and the future Macro Store module.
```sql
CREATE TABLE item_macros (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
item_id UUID REFERENCES items(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
trigger TEXT NOT NULL DEFAULT 'manual',
content TEXT NOT NULL,
revision_number INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(item_id, filename)
);
```
---
## 4. API Endpoints
These endpoints serve the viewport widgets in Create. All are under `/api/items/{partNumber}` and follow the existing auth model.
### 4.1 Metadata
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/metadata` | viewer | Get indexed metadata (schema fields, tags, lifecycle) |
| `PUT` | `/metadata` | editor | Update metadata fields from client |
| `PATCH` | `/metadata/lifecycle` | editor | Transition lifecycle state |
| `PATCH` | `/metadata/tags` | editor | Add/remove tags |
**`GET /api/items/{partNumber}/metadata`**
Returns the indexed metadata for viewport display. This is the fast path -- reads from `item_metadata` rather than downloading and parsing the `.kc` ZIP.
```json
{
"schema_name": "mechanical-part-v2",
"lifecycle_state": "draft",
"tags": ["structural", "aluminum"],
"fields": {
"material": "6061-T6",
"finish": "anodized",
"weight_kg": 0.34,
"category": "bracket"
},
"manifest": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"silo_instance": "https://silo.example.com",
"revision_hash": "a1b2c3d4e5f6",
"kc_version": "1.0"
},
"updated_at": "2026-02-13T20:30:00Z",
"updated_by": "joseph"
}
```
**`PUT /api/items/{partNumber}/metadata`**
Accepts a partial update of schema fields. The server merges into the existing `fields` JSONB. This is the write-back path for the Metadata Editor widget.
```json
{
"fields": {
"material": "7075-T6",
"weight_kg": 0.31
}
}
```
The server validates field names against the schema descriptor. Unknown fields are rejected with `422`.
**`PATCH /api/items/{partNumber}/metadata/lifecycle`**
Transitions lifecycle state. The server validates the transition is permitted (e.g., `draft` -> `review` is allowed, `released` -> `draft` is not without admin override).
```json
{ "state": "review" }
```
### 4.2 Dependencies
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/dependencies` | viewer | Get CAD-extracted dependency list |
| `GET` | `/dependencies/resolve` | viewer | Resolve UUIDs to current part numbers and file status |
**`GET /api/items/{partNumber}/dependencies`**
Returns the raw dependency list from the last `.kc` commit.
**`GET /api/items/{partNumber}/dependencies/resolve`**
Returns the dependency list with each UUID resolved to its current part number, revision, and whether the file exists on disk. This is what the Dependency Table widget calls to populate the status column.
```json
{
"links": [
{
"uuid": "660e8400-...",
"part_number": "KC-BRK-0042",
"label": "Base Plate",
"revision": 2,
"quantity": 1,
"resolved": true,
"file_available": true
},
{
"uuid": "770e8400-...",
"part_number": "KC-HDW-0108",
"label": "M6 SHCS",
"revision": 1,
"quantity": 4,
"resolved": true,
"file_available": true
},
{
"uuid": "880e8400-...",
"part_number": null,
"label": "Cover Panel",
"revision": 1,
"quantity": 1,
"resolved": false,
"file_available": false
}
]
}
```
### 4.3 Approvals
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/approvals` | viewer | Get current approval state |
| `POST` | `/approvals` | editor | Create ECO / start approval workflow |
| `POST` | `/approvals/{id}/sign` | editor | Sign (approve/reject) |
These endpoints power the Approvals Viewer widget. The viewer is read-only in Create -- sign actions happen in the web UI, but the API exists for both.
### 4.4 Macros
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/macros` | viewer | List registered macros |
| `GET` | `/macros/{filename}` | viewer | Get macro source |
Read-only server-side. Macros are authored in Create and committed inside the `.kc`. The server indexes them for discoverability in the future Macro Store.
### 4.5 Existing Endpoints (unchanged)
The viewport widgets also consume these existing endpoints:
| Widget | Endpoint | Purpose |
|--------|----------|---------|
| History Viewer | `GET /api/items/{pn}/revisions` | Full revision list |
| History Viewer | `GET /api/items/{pn}/revisions/compare` | Property diff |
| Job Viewer | `GET /api/jobs?item={pn}&definition={name}&limit=1` | Last job run |
| Job Viewer | `POST /api/jobs` | Trigger job |
| Job Viewer | `GET /api/jobs/{id}/logs` | Job log |
| Manifest Viewer | `GET /api/items/{pn}` | Item details (UUID, etc.) |
No changes needed to these -- they already exist and return the data the widgets need.
---
## 5. Checkout Pipeline
When a client downloads a `.kc` via `GET /api/items/{partNumber}/file`, the server packs current server-side state into the `silo/` directory before serving the file. This ensures the client always gets the latest metadata, even if it was edited via the web UI since the last commit.
### 5.1 Pipeline Steps
```
Client requests file download
|
v
+-----------------------------+
| 1. Read .kc from disk |
+-----------------------------+
|
v
+-----------------------------+
| 2. Pack silo/ from DB |
| - manifest.json (item) |
| - metadata.json (index) |
| - history.json (revs) |
| - approvals.json (ECO) |
| - dependencies.json |
| - macros/ (index) |
| - jobs/ (job defs) |
+-----------------------------+
|
v
+-----------------------------+
| 3. Replace silo/ in ZIP |
| Remove old entries |
| Write packed entries |
+-----------------------------+
|
v
Stream .kc to client
```
### 5.2 Packing Rules
| `silo/` entry | Source | Notes |
|---------------|--------|-------|
| `manifest.json` | `item_metadata` + `items` table | UUID from item, revision_hash from latest revision |
| `metadata.json` | `item_metadata.fields` + tags + lifecycle | Serialized from indexed columns |
| `history.json` | `revisions` table | Last 20 revisions for this item |
| `approvals.json` | `item_approvals` + `approval_signatures` | Current ECO state, omitted if no active ECO |
| `dependencies.json` | `item_dependencies` | Current revision's dependency list |
| `macros/*.py` | `item_macros` | All registered macros |
| `jobs/*.yaml` | `job_definitions` filtered by item type | Job definitions matching this item's trigger filters |
### 5.3 Caching
Packing the `silo/` directory on every download has a cost. To mitigate:
- **ETag header**: The response includes an ETag computed from the revision number + metadata `updated_at`. If the client sends `If-None-Match`, the server can return `304 Not Modified`.
- **Lazy packing**: If the `.kc` blob's `silo/manifest.json` revision_hash matches the current head *and* `item_metadata.updated_at` is older than the blob's upload time, skip repacking entirely -- the blob is already current.
---
## 6. SSE Events
The viewport widgets subscribe to SSE for live updates. These events are broadcast when server-side metadata changes, whether via `.kc` commit, web UI edit, or API call.
| Event | Payload | Trigger |
|-------|---------|---------|
| `metadata.updated` | `{part_number, changed_fields[], lifecycle_state, updated_by}` | Metadata PUT/PATCH |
| `metadata.lifecycle` | `{part_number, from_state, to_state, updated_by}` | Lifecycle transition |
| `metadata.tags` | `{part_number, added[], removed[]}` | Tag add/remove |
| `approval.created` | `{part_number, eco_number, state}` | ECO created |
| `approval.signed` | `{part_number, eco_number, user, role, status}` | Approver action |
| `approval.completed` | `{part_number, eco_number, final_state}` | All approvers acted |
| `dependencies.changed` | `{part_number, added[], removed[], changed[]}` | Dependency diff on commit |
Existing events (`revision.created`, `job.*`, `bom.changed`) continue to work as documented in [SPECIFICATION.md](SPECIFICATION.md) and [WORKERS.md](WORKERS.md).
### 6.1 Widget Subscription Map
| Viewport widget | Subscribes to |
|-----------------|---------------|
| Manifest Viewer | -- (read-only, no live updates) |
| Metadata Editor | `metadata.updated`, `metadata.lifecycle`, `metadata.tags` |
| History Viewer | `revision.created` |
| Approvals Viewer | `approval.created`, `approval.signed`, `approval.completed` |
| Dependency Table | `dependencies.changed` |
| Job Viewer | `job.created`, `job.progress`, `job.completed`, `job.failed` |
| Macro Editor | -- (local-only until committed) |
---
## 7. Web UI Integration
The Silo web UI also benefits from indexed metadata. These are additions to existing pages, not new pages.
### 7.1 Items Page
The item detail panel gains a **Metadata** tab (alongside Main, Properties, Revisions, BOM, Where Used) showing the schema-driven form from `GET /api/items/{pn}/metadata`. Editable for editors.
### 7.2 Items List
New filterable columns: `lifecycle_state`, `tags`. The existing search endpoint gains metadata-aware filtering:
```
GET /api/items?lifecycle=released&tag=aluminum
GET /api/items/search?q=bracket&lifecycle=draft
```
### 7.3 Approvals Page
A new page accessible from the top navigation (visible when a future `approvals` module is enabled). Lists all active ECOs with their approval progress.
---
## 8. Migration
### 8.1 Database Migration
A single migration adds the `item_metadata`, `item_dependencies`, `item_approvals`, `approval_signatures`, and `item_macros` tables. Existing items have no metadata rows -- they're created on first `.kc` commit or via `PUT /api/items/{pn}/metadata`.
### 8.2 Backfill
For items that already have `.kc` files stored on disk (committed before this feature), an admin endpoint re-runs the extraction pipeline:
```
POST /api/admin/reindex-metadata
```
This iterates all items with `.kc` files, opens each ZIP, and indexes the `silo/` contents. Idempotent -- safe to run multiple times.
---
## 9. Implementation Order
| Phase | Server work | Supports client phase |
|-------|------------|----------------------|
| 1 | `item_metadata` table + `GET/PUT /metadata` + commit extraction | SILO_VIEWPORT Phase 1-2 (Manifest, Metadata) |
| 2 | Pack `silo/` on checkout + ETag caching | SILO_VIEWPORT Phase 1-3 |
| 3 | `item_dependencies` table + `/dependencies/resolve` | SILO_VIEWPORT Phase 5 (Dependency Table) |
| 4 | `item_macros` table + `/macros` endpoints | SILO_VIEWPORT Phase 6 (Macro Editor) |
| 5 | `item_approvals` tables + `/approvals` endpoints | SILO_VIEWPORT Phase 7 (Approvals Viewer) |
| 6 | SSE events for metadata/approvals/dependencies | SILO_VIEWPORT Phase 8 (Live integration) |
| 7 | Web UI metadata tab + list filters | Independent of client |
Phases 1-2 are prerequisite for the viewport to work with live data. Phases 3-6 can be built in parallel with client widget development. Phase 7 is web-UI-only and independent.
---
## 10. References
- [SILO_VIEWPORT.md](SILO_VIEWPORT.md) -- Client-side viewport widget specification
- [KC_SPECIFICATION.md](KC_SPECIFICATION.md) -- .kc file format specification
- [SPECIFICATION.md](SPECIFICATION.md) -- Silo server API reference
- [BOM_MERGE.md](BOM_MERGE.md) -- BOM merge rules (dependency reconciliation)
- [WORKERS.md](WORKERS.md) -- Job queue (job viewer data source)
- [MODULES.md](MODULES.md) -- Module system (approval module gating)
- [ROADMAP.md](ROADMAP.md) -- Platform roadmap tiers

View File

@@ -1,749 +0,0 @@
# Module System Specification
**Status:** Draft
**Last Updated:** 2026-02-14
---
## 1. Purpose
Silo's module system defines the boundary between required infrastructure and optional capabilities. Each module groups a set of API endpoints, UI views, and configuration parameters. Modules can be enabled or disabled at runtime by administrators via the web UI, and clients can query which modules are active to adapt their feature set.
The goal: after initial deployment (where `config.yaml` sets database, storage, and server bind), all further operational configuration happens through the admin settings UI. The YAML file becomes the bootstrap; the database becomes the runtime source of truth.
---
## 2. Module Registry
### 2.1 Required Modules
These cannot be disabled. They define what Silo *is*.
| Module ID | Name | Description |
|-----------|------|-------------|
| `core` | Core PDM | Items, revisions, files, BOM, search, import/export, part number generation |
| `schemas` | Schemas | Part numbering schema parsing, segment management, form descriptors |
| `storage` | Storage | MinIO/S3 file storage, presigned uploads, versioning |
### 2.2 Optional Modules
| Module ID | Name | Default | Description |
|-----------|------|---------|-------------|
| `auth` | Authentication | `true` | Local, LDAP, OIDC authentication and RBAC |
| `projects` | Projects | `true` | Project management and item tagging |
| `audit` | Audit | `true` | Audit logging, completeness scoring |
| `odoo` | Odoo ERP | `false` | Odoo integration (config, sync-log, push/pull) |
| `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 |
### 2.3 Module Dependencies
Some modules require others to function:
| Module | Requires |
|--------|----------|
| `dag` | `jobs` |
| `jobs` | `auth` (runner tokens) |
| `odoo` | `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.
---
## 3. Endpoint-to-Module Mapping
### 3.1 `core` (required)
```
# Health
GET /health
GET /ready
# Items
GET /api/items
GET /api/items/search
GET /api/items/by-uuid/{uuid}
GET /api/items/export.csv
GET /api/items/template.csv
GET /api/items/export.ods
GET /api/items/template.ods
POST /api/items
POST /api/items/import
POST /api/items/import.ods
GET /api/items/{partNumber}
PUT /api/items/{partNumber}
DELETE /api/items/{partNumber}
# Revisions
GET /api/items/{partNumber}/revisions
GET /api/items/{partNumber}/revisions/compare
GET /api/items/{partNumber}/revisions/{revision}
POST /api/items/{partNumber}/revisions
PATCH /api/items/{partNumber}/revisions/{revision}
POST /api/items/{partNumber}/revisions/{revision}/rollback
# Files
GET /api/items/{partNumber}/files
GET /api/items/{partNumber}/file
GET /api/items/{partNumber}/file/{revision}
POST /api/items/{partNumber}/file
POST /api/items/{partNumber}/files
DELETE /api/items/{partNumber}/files/{fileId}
PUT /api/items/{partNumber}/thumbnail
POST /api/uploads/presign
# BOM
GET /api/items/{partNumber}/bom
GET /api/items/{partNumber}/bom/expanded
GET /api/items/{partNumber}/bom/flat
GET /api/items/{partNumber}/bom/cost
GET /api/items/{partNumber}/bom/where-used
GET /api/items/{partNumber}/bom/export.csv
GET /api/items/{partNumber}/bom/export.ods
POST /api/items/{partNumber}/bom
POST /api/items/{partNumber}/bom/import
POST /api/items/{partNumber}/bom/merge
PUT /api/items/{partNumber}/bom/{childPartNumber}
DELETE /api/items/{partNumber}/bom/{childPartNumber}
# .kc Metadata
GET /api/items/{partNumber}/metadata
PUT /api/items/{partNumber}/metadata
PATCH /api/items/{partNumber}/metadata/lifecycle
PATCH /api/items/{partNumber}/metadata/tags
# .kc Dependencies
GET /api/items/{partNumber}/dependencies
GET /api/items/{partNumber}/dependencies/resolve
# .kc Macros
GET /api/items/{partNumber}/macros
GET /api/items/{partNumber}/macros/{filename}
# Part Number Generation
POST /api/generate-part-number
# Sheets
POST /api/sheets/diff
# Settings & Modules (admin)
GET /api/modules
GET /api/admin/settings
GET /api/admin/settings/{module}
PUT /api/admin/settings/{module}
POST /api/admin/settings/{module}/test
```
### 3.2 `schemas` (required)
```
GET /api/schemas
GET /api/schemas/{name}
GET /api/schemas/{name}/form
POST /api/schemas/{name}/segments/{segment}/values
PUT /api/schemas/{name}/segments/{segment}/values/{code}
DELETE /api/schemas/{name}/segments/{segment}/values/{code}
```
### 3.3 `storage` (required)
No dedicated endpoints — storage is consumed internally by file upload/download in `core`. Exposed through admin settings for connection status visibility.
### 3.4 `auth`
```
# Public (login flow)
GET /login
POST /login
POST /logout
GET /auth/oidc
GET /auth/callback
# Authenticated
GET /api/auth/me
GET /api/auth/tokens
POST /api/auth/tokens
DELETE /api/auth/tokens/{id}
# Web UI
GET /settings (account info, tokens)
POST /settings/tokens
POST /settings/tokens/{id}/revoke
```
When `auth` is disabled, all routes are open and a synthetic `dev` admin user is injected (current behavior).
### 3.5 `projects`
```
GET /api/projects
GET /api/projects/{code}
GET /api/projects/{code}/items
GET /api/projects/{code}/sheet.ods
POST /api/projects
PUT /api/projects/{code}
DELETE /api/projects/{code}
# Item-project tagging
GET /api/items/{partNumber}/projects
POST /api/items/{partNumber}/projects
DELETE /api/items/{partNumber}/projects/{code}
```
When disabled: project tag endpoints return `404`, project columns are hidden in UI list views, project filter is removed from item search.
### 3.6 `audit`
```
GET /api/audit/completeness
GET /api/audit/completeness/{partNumber}
```
When disabled: audit log table continues to receive writes (it's part of core middleware), but the completeness scoring endpoints and the Audit page in the web UI are hidden. Future: retention policies, export, and compliance reporting endpoints live here.
### 3.7 `odoo`
```
GET /api/integrations/odoo/config
GET /api/integrations/odoo/sync-log
PUT /api/integrations/odoo/config
POST /api/integrations/odoo/test-connection
POST /api/integrations/odoo/sync/push/{partNumber}
POST /api/integrations/odoo/sync/pull/{odooId}
```
### 3.8 `freecad`
No dedicated API endpoints currently. Configures URI scheme and executable path used by the web UI's "Open in Create" links and by CLI operations. Future: client configuration distribution endpoint.
### 3.9 `jobs`
```
# User-facing
GET /api/jobs
GET /api/jobs/{jobID}
GET /api/jobs/{jobID}/logs
POST /api/jobs
POST /api/jobs/{jobID}/cancel
# Job definitions
GET /api/job-definitions
GET /api/job-definitions/{name}
POST /api/job-definitions/reload
# Runner management (admin)
GET /api/runners
POST /api/runners
DELETE /api/runners/{runnerID}
# Runner-facing (runner token auth)
POST /api/runner/heartbeat
POST /api/runner/claim
PUT /api/runner/jobs/{jobID}/progress
POST /api/runner/jobs/{jobID}/complete
POST /api/runner/jobs/{jobID}/fail
POST /api/runner/jobs/{jobID}/log
PUT /api/runner/jobs/{jobID}/dag
```
### 3.10 `dag`
```
GET /api/items/{partNumber}/dag
GET /api/items/{partNumber}/dag/forward-cone/{nodeKey}
GET /api/items/{partNumber}/dag/dirty
PUT /api/items/{partNumber}/dag
POST /api/items/{partNumber}/dag/mark-dirty/{nodeKey}
```
---
## 4. Disabled Module Behavior
When a module is disabled:
1. **API routes** registered by that module return `404 Not Found` with body `{"error": "module '<id>' is not enabled"}`.
2. **Web UI** hides the module's navigation entry, page, and any inline UI elements (e.g., project tags on item cards).
3. **SSE events** from the module are not broadcast.
4. **Background goroutines** (e.g., job timeout sweeper, runner heartbeat checker) are not started.
5. **Database tables** are not dropped — they remain for re-enablement. No data loss on disable/enable cycle.
Implementation: each module's route group is wrapped in a middleware check:
```go
func RequireModule(id string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !modules.IsEnabled(id) {
http.Error(w, `{"error":"module '`+id+`' is not enabled"}`, 404)
return
}
next.ServeHTTP(w, r)
})
}
}
```
---
## 5. Configuration Persistence
### 5.1 Precedence
```
Environment variables (highest — always wins, secrets live here)
Database overrides (admin UI writes here)
config.yaml (lowest — bootstrap defaults)
```
### 5.2 Database Table
```sql
-- Migration 014_settings.sql
CREATE TABLE settings_overrides (
key TEXT PRIMARY KEY, -- dotted path: "auth.ldap.enabled"
value JSONB NOT NULL, -- typed value
updated_by TEXT NOT NULL, -- username
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE module_state (
module_id TEXT PRIMARY KEY, -- "auth", "projects", etc.
enabled BOOLEAN NOT NULL,
updated_by TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
### 5.3 Load Sequence
On startup:
1. Parse `config.yaml` into Go config struct.
2. Query `settings_overrides` — merge each key into the struct using dotted path resolution.
3. Apply environment variable overrides (existing `SILO_*` vars).
4. Query `module_state` — override default enabled/disabled from YAML.
5. Validate module dependencies.
6. Register only enabled modules' route groups.
7. Start only enabled modules' background goroutines.
### 5.4 Runtime Updates
When an admin saves settings via `PUT /api/admin/settings/{module}`:
1. Validate the payload against the module's config schema.
2. Write changed keys to `settings_overrides`.
3. Update `module_state` if `enabled` changed.
4. Apply changes to the in-memory config (hot reload where safe).
5. Broadcast `settings.changed` SSE event with `{module, enabled, changed_keys}`.
6. For changes that require restart (e.g., `server.port`, `database.*`), return a `restart_required: true` flag in the response. The UI shows a banner.
### 5.5 What Requires Restart
| Config Area | Hot Reload | Restart Required |
|-------------|-----------|------------------|
| Module enable/disable | Yes | No |
| `auth.*` provider toggles | Yes | No |
| `auth.cors.allowed_origins` | Yes | No |
| `odoo.*` connection settings | Yes | No |
| `freecad.*` | Yes | No |
| `jobs.*` timeouts, directory | Yes | No |
| `server.host`, `server.port` | No | Yes |
| `database.*` | No | Yes |
| `storage.*` | No | Yes |
| `schemas.directory` | No | Yes |
---
## 6. Public Module Discovery Endpoint
```
GET /api/modules
```
**No authentication required.** Clients need this pre-login to know whether OIDC is available, whether projects exist, etc.
### 6.1 Response
```json
{
"modules": {
"core": {
"enabled": true,
"required": true,
"name": "Core PDM",
"version": "0.2"
},
"schemas": {
"enabled": true,
"required": true,
"name": "Schemas"
},
"storage": {
"enabled": true,
"required": true,
"name": "Storage"
},
"auth": {
"enabled": true,
"required": false,
"name": "Authentication",
"config": {
"local_enabled": true,
"ldap_enabled": true,
"oidc_enabled": true,
"oidc_issuer_url": "https://keycloak.example.com/realms/silo"
}
},
"projects": {
"enabled": true,
"required": false,
"name": "Projects"
},
"audit": {
"enabled": true,
"required": false,
"name": "Audit"
},
"odoo": {
"enabled": false,
"required": false,
"name": "Odoo ERP"
},
"freecad": {
"enabled": true,
"required": false,
"name": "Create Integration",
"config": {
"uri_scheme": "silo"
}
},
"jobs": {
"enabled": false,
"required": false,
"name": "Job Queue"
},
"dag": {
"enabled": false,
"required": false,
"name": "Dependency DAG",
"depends_on": ["jobs"]
}
},
"server": {
"version": "0.2",
"read_only": false
}
}
```
The `config` sub-object exposes only public, non-secret metadata needed by clients. Never includes passwords, tokens, or secret keys.
---
## 7. Admin Settings Endpoints
### 7.1 Get All Settings
```
GET /api/admin/settings
Authorization: Bearer <admin token>
```
Returns full config grouped by module with secrets redacted:
```json
{
"core": {
"server": {
"host": "0.0.0.0",
"port": 8080,
"base_url": "https://silo.example.com",
"read_only": false
}
},
"schemas": {
"directory": "/etc/silo/schemas",
"default": "kindred-rd"
},
"storage": {
"endpoint": "minio:9000",
"bucket": "silo-files",
"access_key": "****",
"secret_key": "****",
"use_ssl": false,
"region": "us-east-1",
"status": "connected"
},
"database": {
"host": "postgres",
"port": 5432,
"name": "silo",
"user": "silo",
"password": "****",
"sslmode": "disable",
"max_connections": 10,
"status": "connected"
},
"auth": {
"enabled": true,
"session_secret": "****",
"local": { "enabled": true },
"ldap": {
"enabled": true,
"url": "ldaps://ipa.example.com",
"base_dn": "dc=kindred,dc=internal",
"user_search_dn": "cn=users,cn=accounts,dc=kindred,dc=internal",
"bind_password": "****",
"role_mapping": { "...": "..." }
},
"oidc": {
"enabled": true,
"issuer_url": "https://keycloak.example.com/realms/silo",
"client_id": "silo",
"client_secret": "****",
"redirect_url": "https://silo.example.com/auth/callback"
},
"cors": { "allowed_origins": ["https://silo.example.com"] }
},
"projects": { "enabled": true },
"audit": { "enabled": true },
"odoo": { "enabled": false, "url": "", "database": "", "username": "" },
"freecad": { "uri_scheme": "silo", "executable": "" },
"jobs": {
"enabled": false,
"directory": "/etc/silo/jobdefs",
"runner_timeout": 90,
"job_timeout_check": 30,
"default_priority": 100
},
"dag": { "enabled": false }
}
```
### 7.2 Get Module Settings
```
GET /api/admin/settings/{module}
```
Returns just the module's config block.
### 7.3 Update Module Settings
```
PUT /api/admin/settings/{module}
Content-Type: application/json
{
"enabled": true,
"ldap": {
"enabled": true,
"url": "ldaps://ipa.example.com"
}
}
```
**Response:**
```json
{
"updated": ["auth.ldap.enabled", "auth.ldap.url"],
"restart_required": false
}
```
### 7.4 Test Connectivity
```
POST /api/admin/settings/{module}/test
```
Available for modules with external connections:
| Module | Test Action |
|--------|------------|
| `storage` | Ping MinIO, verify bucket exists |
| `auth` (ldap) | Attempt LDAP bind with configured credentials |
| `auth` (oidc) | Fetch OIDC discovery document from issuer URL |
| `odoo` | Attempt XML-RPC connection to Odoo |
**Response:**
```json
{
"success": true,
"message": "LDAP bind successful",
"latency_ms": 42
}
```
---
## 8. Config YAML Changes
The existing `config.yaml` gains a `modules` section. Existing top-level keys remain for backward compatibility — the module system reads from both locations.
```yaml
# Existing keys (unchanged, still work)
server:
host: "0.0.0.0"
port: 8080
database:
host: postgres
port: 5432
name: silo
user: silo
password: silodev
sslmode: disable
storage:
endpoint: minio:9000
bucket: silo-files
access_key: silominio
secret_key: silominiosecret
use_ssl: false
schemas:
directory: /etc/silo/schemas
auth:
enabled: true
session_secret: change-me
local:
enabled: true
# New: explicit module toggles (optional, defaults shown)
modules:
projects:
enabled: true
audit:
enabled: true
odoo:
enabled: false
freecad:
enabled: true
uri_scheme: silo
jobs:
enabled: false
directory: /etc/silo/jobdefs
runner_timeout: 90
job_timeout_check: 30
default_priority: 100
dag:
enabled: false
```
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:`).
---
## 9. SSE Events
```
settings.changed {module, enabled, changed_keys[], updated_by}
```
Broadcast on any admin settings change. The web UI listens for this to:
- Show/hide navigation entries when modules are toggled.
- Display a "Settings updated by another admin" toast.
- Show a "Restart required" banner when flagged.
---
## 10. Web UI — Admin Settings Page
The Settings page (`/settings`) is restructured into sections:
### 10.1 Existing (unchanged)
- **Account** — username, display name, email, auth source, role badge.
- **API Tokens** — create, list, revoke.
### 10.2 New: Module Configuration (admin only)
Visible only to admin users. Each module gets a collapsible card:
```
┌─────────────────────────────────────────────────────┐
│ [toggle] Authentication [status] │
├─────────────────────────────────────────────────────┤
│ │
│ ── Local Auth ──────────────────────────────────── │
│ Enabled: [toggle] │
│ │
│ ── LDAP / FreeIPA ──────────────────────────────── │
│ Enabled: [toggle] │
│ URL: [ldaps://ipa.example.com ] │
│ Base DN: [dc=kindred,dc=internal ] [Test] │
│ │
│ ── OIDC / Keycloak ────────────────────────────── │
│ Enabled: [toggle] │
│ Issuer URL: [https://keycloak.example.com] [Test] │
│ Client ID: [silo ] │
│ │
│ ── CORS ────────────────────────────────────────── │
│ Allowed Origins: [tag input] │
│ │
│ [Save] │
└─────────────────────────────────────────────────────┘
```
Module cards for required modules (`core`, `schemas`, `storage`) show their status and config but have no enable/disable toggle.
Status indicators per module:
| Status | Badge | Meaning |
|--------|-------|---------|
| Active | `green` | Enabled and operational |
| Disabled | `overlay1` | Toggled off |
| Error | `red` | Enabled but connectivity or config issue |
| Setup Required | `yellow` | Enabled but missing required config (e.g., LDAP URL empty) |
### 10.3 Infrastructure Section (admin, read-only)
Shows connection status for required infrastructure:
- **Database** — host, port, name, connection pool usage, status badge.
- **Storage** — endpoint, bucket, SSL, status badge.
These are read-only in the UI (setup-only via YAML/env). The "Test" button is available to verify connectivity.
---
## 11. Implementation Order
1. **Migration 014**`settings_overrides` and `module_state` tables.
2. **Config loader refactor** — YAML → DB merge → env override pipeline.
3. **Module registry** — Go struct defining all modules with metadata, dependencies, defaults.
4. **`GET /api/modules`** — public endpoint, no auth.
5. **`RequireModule` middleware** — gate route groups by module state.
6. **Admin settings API**`GET/PUT /api/admin/settings/{module}`, test endpoints.
7. **Web UI settings page** — module cards with toggles, config forms, test buttons.
8. **SSE integration**`settings.changed` event broadcast.
---
## 12. Future Considerations
- **Module manifest format** — per ROADMAP.md, each module will eventually declare routes, views, hooks, and permissions via a manifest. This spec covers the runtime module registry; the manifest format is TBD.
- **Custom modules** — third-party modules that register against the endpoint registry. Requires the manifest contract and a plugin loading mechanism.
- **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.
---
## 13. References
- [CONFIGURATION.md](CONFIGURATION.md) — Current config reference
- [ROADMAP.md](ROADMAP.md) — Module manifest, API endpoint registry
- [AUTH.md](AUTH.md) — Authentication architecture
- [WORKERS.md](WORKERS.md) — Job queue system
- [DAG.md](DAG.md) — Dependency DAG specification
- [SPECIFICATION.md](SPECIFICATION.md) — Full endpoint listing

View File

@@ -88,7 +88,7 @@ Everything depends on these. They define what Silo *is*.
| Component | Description | Status |
|-----------|-------------|--------|
| **Core Silo** | Part/assembly storage, version control, auth, base REST API | Complete |
| **.kc Format Spec** | File format contract between Create and Silo | Complete |
| **.kc Format Spec** | File format contract between Create and Silo | Not Started |
| **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 |
@@ -313,7 +313,7 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
- Rollback functionality
#### File Management
- Filesystem-based file storage
- MinIO integration with versioning
- File upload/download via REST API
- SHA256 checksums for integrity
- Storage path: `items/{partNumber}/rev{N}.FCStd`
@@ -377,8 +377,8 @@ For full SOLIDWORKS PDM comparison tables, see [GAP_ANALYSIS.md Appendix C](GAP_
## Appendix B: Phase 1 Detailed Tasks
### 1.1 File Storage -- COMPLETE
- [x] Filesystem storage backend
### 1.1 MinIO Integration -- COMPLETE
- [x] MinIO service configured in Docker Compose
- [x] File upload via REST API
- [x] File download via REST API (latest and by revision)
- [x] SHA256 checksums on upload

View File

@@ -37,7 +37,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
┌─────────────────────────────────────────────────────────────┐
│ Silo Server (silod) │
│ - REST API (86 endpoints) │
│ - REST API (78 endpoints) │
│ - Authentication (local, LDAP, OIDC) │
│ - Schema parsing and validation │
│ - Part number generation engine │
@@ -49,9 +49,9 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ PostgreSQL │ │ Local Filesystem
│ PostgreSQL │ │ MinIO
│ (psql.example.internal)│ │ - File storage │
│ - Item metadata │ │ - Revision files
│ - Item metadata │ │ - Versioned objects
│ - Relationships │ │ - Thumbnails │
│ - Revision history │ │ │
│ - Auth / Sessions │ │ │
@@ -64,7 +64,7 @@ Silo treats **part numbering schemas as configuration, not code**. Multiple numb
| Component | Technology | Notes |
|-----------|------------|-------|
| Database | PostgreSQL 16 | Existing instance at psql.example.internal |
| File Storage | Local filesystem | Files stored under configurable root directory |
| File Storage | MinIO | S3-compatible, versioning enabled |
| CLI & API Server | Go (1.24) | chi/v5 router, pgx/v5 driver, zerolog |
| Authentication | Multi-backend | Local (bcrypt), LDAP/FreeIPA, OIDC/Keycloak |
| Sessions | PostgreSQL pgxstore | alexedwards/scs, 24h lifetime |
@@ -83,7 +83,7 @@ An **item** is the fundamental entity. Items have:
- **Properties** (key-value pairs, schema-defined and custom)
- **Relationships** to other items
- **Revisions** (append-only history)
- **Files** (optional, stored on the local filesystem)
- **Files** (optional, stored in MinIO)
- **Location** (optional physical inventory location)
### 3.2 Database Schema (Conceptual)
@@ -115,7 +115,7 @@ CREATE TABLE revisions (
item_id UUID REFERENCES items(id) NOT NULL,
revision_number INTEGER NOT NULL,
properties JSONB NOT NULL, -- all properties at this revision
file_version TEXT, -- storage version ID if applicable
file_version TEXT, -- MinIO version ID if applicable
created_at TIMESTAMPTZ DEFAULT now(),
created_by TEXT, -- user identifier (future: LDAP DN)
comment TEXT,
@@ -345,7 +345,7 @@ CAD workbench and spreadsheet extension implementations are maintained in separa
### 5.1 File Storage Strategy
Files are stored on the local filesystem under a configurable root directory. Storage path convention: `items/{partNumber}/rev{N}.ext`. SHA-256 checksums are captured on upload for integrity verification.
Files are stored as whole objects in MinIO with versioning enabled. Storage path convention: `items/{partNumber}/rev{N}.ext`. SHA-256 checksums are captured on upload for integrity verification.
Future option: exploded storage (unpack ZIP-based CAD archives for better diffing).
@@ -439,7 +439,7 @@ Revisions are created explicitly by user action (not automatic):
### 7.3 Revision vs. File Version
- **Revision**: Silo metadata revision (tracked in PostgreSQL)
- **File Version**: File on disk corresponding to a revision
- **File Version**: MinIO object version (automatic on upload)
A single Silo revision may span multiple file uploads during editing. Only committed revisions create formal revision records.
@@ -598,12 +598,12 @@ See [AUTH.md](AUTH.md) for full architecture details and [AUTH_USER_GUIDE.md](AU
## 11. API Design
### 11.1 REST Endpoints (86 Implemented)
### 11.1 REST Endpoints (78 Implemented)
```
# Health (no auth)
GET /health # Basic health check
GET /ready # Readiness (DB)
GET /ready # Readiness (DB + MinIO)
# Auth (no auth required)
GET /login # Login page
@@ -624,8 +624,8 @@ GET /api/auth/tokens # List user's API to
POST /api/auth/tokens # Create API token
DELETE /api/auth/tokens/{id} # Revoke API token
# Direct Uploads (editor)
POST /api/uploads/presign # Get upload URL [editor]
# Presigned Uploads (editor)
POST /api/uploads/presign # Get presigned MinIO upload URL [editor]
# Schemas (read: viewer, write: editor)
GET /api/schemas # List all schemas
@@ -697,20 +697,6 @@ POST /api/items/{partNumber}/bom/merge # Merge BOM from ODS
PUT /api/items/{partNumber}/bom/{childPartNumber} # Update BOM entry [editor]
DELETE /api/items/{partNumber}/bom/{childPartNumber} # Remove BOM entry [editor]
# .kc Metadata (read: viewer, write: editor)
GET /api/items/{partNumber}/metadata # Get indexed .kc metadata
PUT /api/items/{partNumber}/metadata # Update metadata fields [editor]
PATCH /api/items/{partNumber}/metadata/lifecycle # Transition lifecycle state [editor]
PATCH /api/items/{partNumber}/metadata/tags # Add/remove tags [editor]
# .kc Dependencies (viewer)
GET /api/items/{partNumber}/dependencies # List raw dependencies
GET /api/items/{partNumber}/dependencies/resolve # Resolve UUIDs to part numbers + file availability
# .kc Macros (viewer)
GET /api/items/{partNumber}/macros # List registered macros
GET /api/items/{partNumber}/macros/{filename} # Get macro source content
# Audit (viewer)
GET /api/audit/completeness # Item completeness scores
GET /api/audit/completeness/{partNumber} # Item detail breakdown
@@ -749,139 +735,6 @@ POST /api/inventory/{partNumber}/move
---
## 11.3 .kc File Integration
Silo supports the `.kc` file format — a ZIP archive that is a superset of FreeCAD's `.fcstd`. A `.kc` file contains everything an `.fcstd` does, plus a `silo/` directory with platform metadata.
#### Standard entries (preserved as-is)
`Document.xml`, `GuiDocument.xml`, BREP geometry files (`.brp`), `thumbnails/`
#### Silo entries (`silo/` directory)
| Path | Purpose |
|------|---------|
| `silo/manifest.json` | Instance origin, part UUID, revision hash, `.kc` schema version |
| `silo/metadata.json` | Custom schema field values, tags, lifecycle state |
| `silo/history.json` | Local revision log (server-generated on checkout) |
| `silo/dependencies.json` | Assembly link references by Silo UUID |
| `silo/macros/*.py` | Embedded macro scripts bound to this part |
#### Commit-time extraction
When a `.kc` file is uploaded via `POST /api/items/{partNumber}/file`, the server:
1. Opens the ZIP and scans for `silo/` entries
2. Parses `silo/manifest.json` and validates the UUID matches the item
3. Upserts `silo/metadata.json` fields into the `item_metadata` table
4. Replaces `silo/dependencies.json` entries in the `item_dependencies` table
5. Replaces `silo/macros/*.py` entries in the `item_macros` table
6. Broadcasts SSE events: `metadata.updated`, `dependencies.changed`, `macros.changed`
Extraction is best-effort — failures are logged as warnings but do not block the upload.
#### Checkout-time packing
When a `.kc` file is downloaded via `GET /api/items/{partNumber}/file/{revision}`, the server repacks the `silo/` directory with current database state:
- `silo/manifest.json` — current item UUID and metadata freshness
- `silo/metadata.json` — latest schema fields, tags, lifecycle state
- `silo/history.json` — last 20 revisions from the database
- `silo/dependencies.json` — current dependency list from `item_dependencies`
Non-silo ZIP entries are passed through unchanged. If the file is a plain `.fcstd` (no `silo/` directory), it is served as-is.
ETag caching: the server computes an ETag from `revision_number:metadata.updated_at` and returns `304 Not Modified` when the client's `If-None-Match` header matches.
#### Lifecycle state machine
The `lifecycle_state` field in `item_metadata` follows this state machine:
```
draft → review → released → obsolete
↑ ↓
└────────┘
```
Valid transitions are enforced by `PATCH /metadata/lifecycle`. Invalid transitions return `422 Unprocessable Entity`.
#### Metadata response shape
```json
{
"schema_name": "kindred-rd",
"lifecycle_state": "draft",
"tags": ["prototype", "v2"],
"fields": {"material": "AL6061", "finish": "anodized"},
"manifest": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"silo_instance": "silo.example.com",
"revision_hash": "abc123",
"kc_version": "1.0"
},
"updated_at": "2026-02-18T12:00:00Z",
"updated_by": "forbes"
}
```
#### Dependency response shape
```json
[
{
"uuid": "550e8400-...",
"part_number": "F01-0042",
"revision": 3,
"quantity": 4.0,
"label": "M5 Bolt",
"relationship": "component"
}
]
```
#### Resolved dependency response shape
```json
[
{
"uuid": "550e8400-...",
"part_number": "F01-0042",
"label": "M5 Bolt",
"revision": 3,
"quantity": 4.0,
"resolved": true,
"file_available": true
}
]
```
#### Macro list response shape
```json
[
{"filename": "validate_dims.py", "trigger": "manual", "revision_number": 5}
]
```
#### Macro detail response shape
```json
{
"filename": "validate_dims.py",
"trigger": "manual",
"content": "import FreeCAD\n...",
"revision_number": 5
}
```
#### Database tables (migration 018)
- `item_metadata` — schema fields, lifecycle state, tags, manifest info
- `item_dependencies` — parent/child UUID references with quantity and relationship type
- `item_macros` — filename, trigger type, source content, indexed per item
---
## 12. MVP Scope
### 12.1 Implemented
@@ -890,8 +743,8 @@ Valid transitions are enforced by `PATCH /metadata/lifecycle`. Invalid transitio
- [x] YAML schema parser for part numbering
- [x] Part number generation engine
- [x] CLI tool (`cmd/silo`)
- [x] API server (`cmd/silod`) with 86 endpoints
- [x] Filesystem-based file storage
- [x] API server (`cmd/silod`) with 78 endpoints
- [x] MinIO integration for file storage with versioning
- [x] BOM relationships (component, alternate, reference)
- [x] Multi-level BOM (recursive expansion with configurable depth)
- [x] Where-used queries (reverse parent lookup)
@@ -912,12 +765,6 @@ Valid transitions are enforced by `PATCH /metadata/lifecycle`. Invalid transitio
- [x] Audit logging and completeness scoring
- [x] CSRF protection (nosurf)
- [x] Fuzzy search
- [x] .kc file extraction pipeline (metadata, dependencies, macros indexed on commit)
- [x] .kc file packing on checkout (manifest, metadata, history, dependencies)
- [x] .kc metadata API (get, update fields, lifecycle transitions, tags)
- [x] .kc dependency API (list, resolve with file availability)
- [x] .kc macro API (list, get source content)
- [x] ETag caching for .kc file downloads
- [x] Property schema versioning framework
- [x] Docker Compose deployment (dev and prod)
- [x] systemd service and deployment scripts

View File

@@ -10,12 +10,12 @@
| Component | Status | Notes |
|-----------|--------|-------|
| PostgreSQL schema | Complete | 18 migrations applied |
| PostgreSQL schema | Complete | 13 migrations applied |
| YAML schema parser | Complete | Supports enum, serial, constant, string segments |
| Part number generator | Complete | Scoped sequences, category-based format |
| API server (`silod`) | Complete | 86 REST endpoints via chi/v5 |
| API server (`silod`) | Complete | 78 REST endpoints via chi/v5 |
| CLI tool (`silo`) | Complete | Item registration and management |
| Filesystem file storage | Complete | Upload, download, checksums |
| MinIO file storage | Complete | Upload, download, versioning, checksums |
| Revision control | Complete | Append-only history, rollback, comparison, status/labels |
| Project management | Complete | CRUD, many-to-many item tagging |
| CSV import/export | Complete | Dry-run validation, template generation |
@@ -29,12 +29,7 @@
| CSRF protection | Complete | nosurf on web forms |
| Fuzzy search | Complete | sahilm/fuzzy library |
| Web UI | Complete | React SPA (Vite + TypeScript), 6 pages, Catppuccin Mocha theme |
| File attachments | Complete | Direct uploads, item file association, thumbnails |
| .kc extraction pipeline | Complete | Metadata, dependencies, macros indexed on commit |
| .kc checkout packing | Complete | Manifest, metadata, history, dependencies repacked on download |
| .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 |
| File attachments | Complete | Presigned uploads, item file association, thumbnails |
| Odoo ERP integration | Partial | Config and sync-log CRUD functional; push/pull are stubs |
| Docker Compose | Complete | Dev and production configurations |
| Deployment scripts | Complete | setup-host, deploy, init-db, setup-ipa-nginx |
@@ -61,7 +56,7 @@ FreeCAD workbench and LibreOffice Calc extension are maintained in separate repo
| Service | Host | Status |
|---------|------|--------|
| PostgreSQL | psql.example.internal:5432 | Running |
| File Storage | /opt/silo/data (filesystem) | Configured |
| MinIO | localhost:9000 (API) / :9001 (console) | Configured |
| Silo API | localhost:8080 | Builds successfully |
---
@@ -101,8 +96,3 @@ The schema defines 170 category codes across 10 groups:
| 011_item_files.sql | Item file attachments (item_files table, thumbnail_key column) |
| 012_bom_source.sql | BOM entry source tracking |
| 013_move_cost_sourcing_to_props.sql | Move sourcing_link and standard_cost from item columns to revision properties |
| 014_settings.sql | Settings overrides and module state tables |
| 015_jobs.sql | Job queue, runner, and job log tables |
| 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) |

View File

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

After

Width:  |  Height:  |  Size: 675 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 442 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 441 B

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