- Add Dependency type to internal/kc and extract silo/dependencies.json
from .kc files on commit
- Create ItemDependencyRepository with ReplaceForRevision, ListByItem,
and Resolve (LEFT JOIN against items table)
- Add GET /{partNumber}/dependencies and
GET /{partNumber}/dependencies/resolve endpoints
- Index dependencies in extractKCMetadata with SSE broadcast
- Pack real dependency data into .kc files on checkout
- Update PackInput.Dependencies from []any to []Dependency
Closes#143
Implements issue #142 — .kc checkout pipeline that repacks silo/ entries
with current DB state before serving downloads.
When a client downloads a .kc file via GET /api/items/{pn}/file/{rev},
the server now:
1. Reads the file from storage into memory
2. Checks for silo/ directory (plain .fcstd files bypass packing)
3. Repacks silo/ entries with current item_metadata + revision history
4. Streams the repacked ZIP to the client
New files:
- internal/kc/pack.go: Pack() replaces silo/ entries in ZIP, preserving
all non-silo entries (FreeCAD files, thumbnails) with original
compression and timestamps. HasSiloDir() for lightweight detection.
- internal/api/pack_handlers.go: packKCFile server helper, computeETag,
canSkipRepack lazy optimization.
ETag caching:
- ETag computed from revision_number + metadata.updated_at
- If-None-Match support returns 304 Not Modified before reading storage
- Cache-Control: private, must-revalidate
Lazy packing optimization:
- Skips repack if revision_hash matches and metadata unchanged since upload
Phase 2 packs: manifest.json, metadata.json, history.json,
dependencies.json (empty []). Approvals, macros, jobs deferred to
Phase 3-5.
Closes#142
Implements issue #141 — .kc server-side metadata integration Phase 1.
When a .kc file is uploaded, the server extracts silo/manifest.json and
silo/metadata.json from the ZIP archive and indexes them into the
item_metadata table. Plain .fcstd files continue to work unchanged.
Extraction is best-effort: failures are logged but do not block the upload.
New packages:
- internal/kc: ZIP extraction library (Extract, Manifest, Metadata types)
- internal/db: ItemMetadataRepository (Get, Upsert, UpdateFields,
UpdateLifecycle, SetTags)
New API endpoints under /api/items/{partNumber}:
- GET /metadata — read indexed metadata (viewer)
- PUT /metadata — merge fields into JSONB (editor)
- PATCH /metadata/lifecycle — transition lifecycle state (editor)
- PATCH /metadata/tags — add/remove tags (editor)
SSE events: metadata.updated, metadata.lifecycle, metadata.tags
Lifecycle transitions (Phase 1): draft→review→released→obsolete,
review→draft (reject).
Closes#141
Adds scripts/migrate-storage.sh that follows the same deploy.sh pattern:
cross-compiles the migrate-storage binary locally, uploads it to the
target host via SCP, then runs it over SSH using credentials from
/etc/silo/silod.env.
Usage: ./scripts/migrate-storage.sh <silo-host> <psql-host> <minio-host> [flags...]
Standalone binary (cmd/migrate-storage) that downloads all files from
MinIO and writes them to the local filesystem for decommissioning MinIO.
Queries revision files, item file attachments, and item thumbnails from
the database, then downloads each from MinIO preserving the object key
structure as filesystem paths. Supports --dry-run, --verbose, atomic
writes via temp+rename, and idempotent re-runs (skips existing files
with matching size).
Add three new endpoints that bypass the MinIO presigned URL flow:
- POST /api/items/{pn}/files/upload — multipart file upload
- POST /api/items/{pn}/thumbnail/upload — multipart thumbnail upload
- GET /api/items/{pn}/files/{fileId}/download — stream file download
Rewrite frontend upload flow: files are held in browser memory on drop
and uploaded directly after item creation via multipart POST. The old
presign+associate endpoints remain for MinIO backward compatibility.
Closes#129
Add storage_backend columns to track which backend (minio or filesystem)
holds each file, enabling dual-running during migration.
Migration 017_file_storage_metadata.sql:
- item_files.storage_backend TEXT NOT NULL DEFAULT 'minio'
- revisions.file_storage_backend TEXT NOT NULL DEFAULT 'minio'
DB repository changes:
- Revision struct: add FileStorageBackend field
- ItemFile struct: add StorageBackend field
- All INSERT queries include the new columns
- All SELECT queries read them (COALESCE for pre-migration compat)
- CreateRevisionFromExisting copies the backend from source revision
- Default to 'minio' when field is empty (backward compat)
Existing rows default to 'minio'. New uploads will write 'filesystem'
when the filesystem backend is active.
Closes#128
Implement FilesystemStore satisfying the FileStore interface for local
filesystem storage, replacing MinIO for simpler deployments.
- Atomic writes via temp file + os.Rename (no partial files)
- SHA-256 checksum computed on Put via io.MultiWriter
- Get/GetVersion return os.File (GetVersion ignores versionID)
- Delete is idempotent (no error if file missing)
- Copy uses same atomic write pattern
- PresignPut returns ErrPresignNotSupported
- Ping verifies root directory is writable
- Wire NewFilesystemStore in main.go backend switch
- 14 unit tests covering all methods including atomicity
Closes#127
Extract a FileStore interface from the concrete *storage.Storage MinIO
wrapper so the API layer is storage-backend agnostic.
- Define FileStore interface in internal/storage/interface.go
- Add Exists method to MinIO Storage (via StatObject)
- Add compile-time interface satisfaction check
- Change Server.storage and ServerState.storage to FileStore interface
- Update NewServer and NewServerState signatures
- Add Backend and FilesystemConfig fields to StorageConfig
- Add backend selection switch in main.go (minio/filesystem/unknown)
- Update config.example.yaml with backend field
The nil-interface pattern is preserved: when storage is unconfigured,
store remains a true nil FileStore (not a typed nil pointer), so all
existing if s.storage == nil checks continue to work correctly.
Closes#126
Add useSSE hook that connects to /api/events with automatic reconnect
and exponential backoff. On settings.changed events:
- Refresh module state so sidebar nav items show/hide immediately
- Show dismissable toast when another admin updates settings
The backend already publishes settings.changed in HandleUpdateModuleSettings.
Closes#101
Relocate Edit and Delete buttons from the header row into the tab bar,
grouping them with tab navigation to reduce mouse travel. Adds Pencil
and Trash2 icons for quick visual recognition.
Header now only shows part number, type badge, and close button.
Closes#119
- Add kindred-logo.svg as site favicon (#115)
- Narrow settings page to 66% max-width, centered (#116)
- Add max-height and scroll to API token table (#118)
Closes#115, closes#116, closes#118
- Replace top header with left sidebar navigation
- Sidebar shows module-aware nav items filtered by /api/modules
- Collapsible: expanded shows icon+label, collapsed shows icon only
- Toggle with Ctrl+J or collapse button, state persisted in localStorage
- Keyboard navigable: Arrow Up/Down, Enter to navigate, Escape to collapse
- Bottom section: density toggle, user info with role badge, logout
- Add useModules hook for fetching module state
- Add sidebar density variables to theme.css
Closes#113, closes#114
Add LocationRepository with CRUD operations, hierarchy traversal
(children, subtree by path prefix), and inventory-safe deletion.
Endpoints:
GET /api/locations — list all or ?tree={path} for subtree
POST /api/locations — create (auto-resolves parent_id, depth)
GET /api/locations/{path..} — get by hierarchical path
PUT /api/locations/{path..} — update name, type, metadata
DELETE /api/locations/{path..} — delete (rejects if inventory exists)
Uses chi wildcard routes to support multi-segment paths like
/api/locations/lab/shelf-a/bin-3.
Includes 10 handler integration tests covering CRUD, nesting,
validation, duplicates, tree queries, and delete-not-found.
Closes#81
Add admin-only Module Configuration section to the Settings page.
Each module gets a collapsible card with enable/disable toggle,
status badge, module-specific config fields, save and test
connectivity buttons.
- AdminModules: fetches GET /api/modules + GET /api/admin/settings,
renders Infrastructure and Features groups, restart banner
- ModuleCard: collapsible card with toggle, status badge, field
layouts per module, save (PUT) and test (POST) actions
- TypeScript types for ModuleInfo, ModulesResponse, admin settings
API response shapes
Ref: #100
Add four admin-only endpoints under /api/admin/settings:
- GET / — full config (secrets redacted)
- GET /{module} — single module config
- PUT /{module} — toggle modules + persist config overrides
- POST /{module}/test — test external connectivity (database, storage)
PUT publishes a settings.changed SSE event. Config overrides are
persisted for future hot-reload support; changes to database/storage/
server/schemas namespaces return restart_required: true.
Wires SettingsRepository into Server struct.
Closes#99
Add HandleGetModules returning module state, metadata, and
public config (auth providers, Create URI scheme). No auth
required — clients call this pre-login.
Register at /api/modules before the auth middleware.
Ref #97
Add modules.Registry and config.Config fields to Server struct.
Create registry in main.go, load state from YAML+DB, log all
module states at startup.
Conditionally start job/runner sweeper goroutines only when the
jobs module is enabled.
Update all 5 test files to pass registry to NewServer.
Ref #95, #96
Add migration 016 with two tables for the module system:
- settings_overrides: dotted-path config overrides set via admin UI
- module_state: per-module enabled/disabled state
Update testutil.TruncateAll to include new tables.
Ref #94