Architecture split: extract PrivStack.Services and PrivStack.Server
Details
Phase 0 of the headless server architecture split. Extracts ~155 service
files with zero Avalonia dependency into a shared PrivStack.Services
library and creates a standalone PrivStack.Server headless binary.
Architecture changes:
- New PrivStack.Services project: shared core services, models, native
FFI, AI services, API server, biometric, connections, file sync, IPC,
plugin host, update services — all Avalonia-free
- New PrivStack.Server project: headless console binary (privstack-server)
that references Services + Sdk without any Avalonia dependency
- Desktop now references Services and only contains UI-specific code:
views, view models, theme/font/layout services, dialog service,
backup service, whisper/audio, spell check, plugin registry
Key abstractions created:
- INavigationHost: abstracts MainWindowViewModel navigation for Services
- IEmbeddingService: abstracts ONNX embedding (Desktop implements,
Server stubs)
- IWindowSettingsService: Desktop-only Window position/size persistence
- DesktopAppSettingsService: extends base AppSettingsService with
Avalonia Window operations
- Extensible AI provider registration via constructor injection
Breaking changes to models (Avalonia-free):
- SyncModels: DiscoveryBrush (IBrush) → DiscoveryCategory (string)
- CloudSyncModels: SeverityBrush (IBrush) → SeverityLevel (string)
- IAppSettingsService: removed ApplyToWindow/UpdateWindowBounds
- IDialogService: removed SetOwner/Owner
- IPluginRegistry: SetMainViewModel/GetMainViewModel use object?
Dependency graph:
Sdk (no deps)
↑
Services → Sdk
↑ ↑
Desktop → Services + Sdk + UI.Adaptive + Avalonia
Server → Services + Sdk (no Avalonia)
Add headless API mode (--headless flag)
Details
PrivStack can now run as a headless API server without the Avalonia GUI,
enabling programmatic access via scripts, CI/CD, or background services.
CLI interface:
privstack --headless [--workspace <name>] [--port <N>] [--bind <addr>]
privstack --headless --show-api-key
privstack --headless --generate-api-key
New files:
- HeadlessOptions: parsed CLI arguments record
- HeadlessHost: startup orchestrator (workspace resolution, auth via
PRIVSTACK_MASTER_PASSWORD env var or stdin prompt, plugin discovery,
API server lifecycle, graceful SIGTERM/SIGINT shutdown)
- HeadlessStubs: no-op implementations for 8 UI service interfaces
(dispatcher, dialogs, toast, theme, font scale, layout, notifications,
focus mode)
Modified files:
- Program.cs: Main() returns int exit codes, branches to HeadlessHost
when --headless detected via hand-rolled arg parser
- ServiceRegistration: extracted RegisterCoreServices() + WireCorePostBuild()
shared methods, added ConfigureHeadless() with stub registrations and
cached-password vault unlock
- App.axaml.cs: Services setter changed to internal for HeadlessHost access
- ILocalApiServer/LocalApiServer: added BindAddress property, non-localhost
Kestrel binding support, workspace name/ID in /api/v1/status response
Exit codes: 0=success, 1=config, 2=auth, 3=port-in-use, 4=db-locked
Replace endpoint detail child window with inline ModalOverlay
Details
The ApiRouteDetailWindow was a separate child window shown via
ShowDialog(this) — on macOS, child dialogs are constrained to the parent
window's bounds, causing long response content to get clipped with no
way to scroll further.
Replaced with the shared ModalOverlay control from PrivStack.UI.Adaptive,
rendered inline inside ApiDocsWindow via a Panel wrapper. Route detail
content (cURL, params, request/response) is now built directly into the
overlay body. Deleted ApiRouteDetailWindow entirely.
Get notified about new releases