feat: Add initial deployment setup for OTSSignsOrchestrator

- Create index.html for the web application interface.
- Implement deploy.sh script for building and deploying the application to a Docker Swarm manager.
- Add docker-compose.yml for defining application and PostgreSQL service configurations.
This commit is contained in:
Matt Batchelder
2026-03-23 21:28:14 -04:00
parent c6d46098dd
commit 9a35e40083
240 changed files with 11806 additions and 10828 deletions

View File

@@ -2,27 +2,21 @@
## Architecture
Layered MVVM desktop app for provisioning and managing Xibo CMS instances on Docker Swarm.
Web-based system for provisioning and managing Xibo CMS instances on Docker Swarm.
- **OTSSignsOrchestrator.Core** — class library: services, EF Core data access, models, configuration. Reusable across UIs.
- **OTSSignsOrchestrator.Desktop** — Avalonia 11 UI: views, view models, DI setup. References Core.
- **templates/** — Docker Compose + PHP templates with `{{PLACEHOLDER}}` substitution.
- **OTSSignsOrchestrator** — ASP.NET Core API + React web UI (Vite + TypeScript + Tailwind CSS) + SignalR + Quartz scheduler. PostgreSQL 16. Contains all services, models, configuration, and business logic.
- **OTSSignsOrchestrator.Tests** — xUnit test project.
### Key patterns
- Services injected via `IServiceProvider` (registered in `App.axaml.cs``ConfigureServices()`)
- Singletons: stateful services (SSH connections, Docker CLI). Transient: stateless logic.
- Services injected via DI (registered in `Program.cs`)
- Singletons: stateful services (SSH connection factory). Scoped: per-request services (Docker CLI, secrets). Transient: stateless logic.
- Configuration via `IOptions<T>` bound from `appsettings.json` (see `AppOptions.cs` for all sections).
- Bitwarden Secrets Manager is the source of truth for all sensitive config. `SettingsService` caches in-memory.
- Local SQLite DB (`otssigns-desktop.db`) stores SSH hosts + operation logs. Credentials encrypted via Data Protection API.
### Scope & file discipline
**The Server project is net-new — keep concerns separated.**
- Never modify `OTSSignsOrchestrator.Core` or `OTSSignsOrchestrator.Desktop` unless the prompt explicitly says to.
- When in doubt, add new code to `OTSSignsOrchestrator.Server`.
- Never modify `XiboContext.cs` without explicit instruction.
- PostgreSQL database via `OrchestratorDbContext`. Credentials encrypted via Data Protection API.
- React frontend in `ClientApp/`, built to `wwwroot/` via Vite. Cookie-based JWT auth.
### External integrations
Xibo CMS REST API (OAuth2), Authentik SAML IdP, Bitwarden Secrets SDK, Docker Swarm (via SSH), Git (LibGit2Sharp), MySQL 8.4, NFS volumes, Pangolin/Newt VPN.
Xibo CMS REST API (OAuth2), Authentik SAML IdP, Bitwarden Secrets SDK, Docker Swarm (via SSH), Git (LibGit2Sharp), MySQL 8.4, NFS volumes, Pangolin/Newt VPN, Stripe, SendGrid.
#### Xibo API rules — non-negotiable
- `GET /api/application` is **BLOCKED**. Only POST and DELETE exist.
@@ -43,17 +37,20 @@ Xibo CMS REST API (OAuth2), Authentik SAML IdP, Bitwarden Secrets SDK, Docker Sw
```bash
# Build
dotnet build OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
dotnet build
# Run
dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
# Run Server
dotnet run --project OTSSignsOrchestrator/OTSSignsOrchestrator.csproj
# No test suite currently — no xUnit/NUnit projects
# Run tests
dotnet test OTSSignsOrchestrator.Tests/OTSSignsOrchestrator.Tests.csproj
# Frontend dev
cd OTSSignsOrchestrator/ClientApp && npm run dev
```
- .NET 9.0, Avalonia 11.2.3, CommunityToolkit.Mvvm 8.4
- Runtime identifiers: `linux-x64`, `win-x64`, `osx-x64`, `osx-arm64`
- EF Core migrations in `OTSSignsOrchestrator.Core/Migrations/`
- .NET 9.0, React 18, Vite, TypeScript, Tailwind CSS, shadcn/ui
- EF Core migrations in `OTSSignsOrchestrator/Migrations/`
### Test coverage non-negotiables
Unit tests are **required** for:
@@ -67,28 +64,10 @@ Integration tests **require** Testcontainers with a real PostgreSQL 16 instance
## Conventions
### ViewModels
- Inherit `ObservableObject` (CommunityToolkit.Mvvm). Use `[ObservableProperty]` for bindable fields and `[RelayCommand]` for commands.
- React to changes via `partial void OnXxxChanged(T value)` methods generated by the toolkit.
- Resolve services from `IServiceProvider` in constructors. Navigation via `MainWindowViewModel.CurrentView`.
- Confirmation dialogs use `Func<string, string, Task<bool>> ConfirmAsync` property — wired by the View.
### Avalonia threading — critical for stability
All SignalR message handlers and background thread continuations that touch `ObservableProperty` or `ObservableCollection` **MUST** be wrapped in:
```csharp
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { ... });
```
**Failure to do this causes silent cross-thread exceptions in Avalonia.** Never suggest direct property assignment from a non-UI thread.
### Views (Avalonia XAML)
- Compiled bindings enabled (`x:CompileBindings="True"`). DataTemplates in `MainWindow.axaml` map ViewModel types to View UserControls.
- Layout: DockPanel with status bar (bottom), sidebar nav (left), dynamic ContentControl (center).
- Style: Fluent theme, dark palette (`#0C0C14` accents).
### Services
- Interface + implementation pattern for testable services (`IXiboApiService`, `IDockerCliService`, etc.).
- `SshDockerCliService` is a singleton**must call `SetHost(host)` before each operation** in loops.
- All long operations are `async Task`. Use `IsBusy` + `StatusMessage` properties for UI feedback.
- `SshDockerCliService` is scoped**must call `SetHost(host)` before each operation** in loops.
- All long operations are `async Task`.
### Naming
- Customer abbreviation: exactly 3 lowercase letters (`^[a-z]{3}$`).
@@ -97,27 +76,23 @@ Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { ... });
- `AppConstants.SanitizeName()` filters to `[a-z0-9_-]`.
### Credential handling
Never store OAuth2 client secrets, Stripe keys, or SSH passwords in the database. Secrets go to the Bitwarden CLI wrapper only. `OauthAppRegistry` stores `clientId` only — never the secret. Log credentials to `JobStep` output **ONLY** as a last-resort break-glass fallback, and mark it explicitly as emergency recovery data in the log.
Never store OAuth2 client secrets, Stripe keys, or SSH passwords in the database. Secrets go to the Bitwarden CLI wrapper only. `OauthAppRegistry` stores `clientId` only — never the secret.
### Code generation verification
After generating any class that implements an interface, **verify all interface members are implemented.** After generating any pipeline, **verify all steps are implemented as `JobStep` entities with progress broadcast via `IHubContext<FleetHub>`.** Do not stub steps as TODO — implement them fully or flag explicitly that the step requires external infrastructure access that cannot be completed in this context.
After generating any class that implements an interface, **verify all interface members are implemented.** After generating any pipeline, **verify all steps are implemented as `JobStep` entities with progress broadcast via `IHubContext<FleetHub>`.** Do not stub steps as TODO — implement them fully or flag explicitly.
### Data layer
- Entities in `Core/Models/Entities/`, DTOs in `Core/Models/DTOs/`.
- `XiboContext` applies unique index on `SshHost.Label` and encrypts credential fields.
- Add new migrations via: `dotnet ef migrations add <Name> --project OTSSignsOrchestrator.Core --startup-project OTSSignsOrchestrator.Desktop`
- Entities in `Core/Models/Entities/` and `Server/Data/Entities/`.
- DTOs in `Core/Models/DTOs/`.
- `OrchestratorDbContext` is the primary database context (PostgreSQL).
### Immutability enforcement
**AuditLog, Message, and Evidence are append-only by design.** Never generate `Update()` or `Delete()` methods on these repositories. Add an explicit comment on each repository class:
```csharp
// IMMUTABLE — no update or delete operations permitted.
```
**AuditLog, Message, and Evidence are append-only by design.** Never generate `Update()` or `Delete()` methods on these repositories.
## Pitfalls
- **SSH singleton state**: `SshDockerCliService.SetHost()` must be called before each host operation — stale host causes silent failures.
- **SSH host state**: `SshDockerCliService.SetHost()` must be called before each host operation — stale host causes silent failures.
- **Bitwarden cache**: After creating secrets during deployment, call `FlushCacheAsync()` before reading them back.
- **Data Protection keys**: Stored in `%APPDATA%/OTSSignsOrchestrator/keys`. If lost, encrypted SSH passwords are unrecoverable.
- **Docker volumes are sticky**: Failed deploys leave volumes with old NFS driver options. Use `PurgeStaleVolumes: true` to force fresh volumes (causes data loss).
- **No saga/rollback**: Instance creation spans Git → MySQL → Docker → Xibo. Partial failures leave orphaned resources; cleanup is manual via `OperationLog`.
- **Template CIFS→NFS compat**: Old `{{CIFS_*}}` tokens still render correctly as NFS equivalents.