# Project Guidelines — OTS Signs Orchestrator ## Architecture Layered MVVM desktop app 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. ### Key patterns - Services injected via `IServiceProvider` (registered in `App.axaml.cs` → `ConfigureServices()`) - Singletons: stateful services (SSH connections, Docker CLI). Transient: stateless logic. - Configuration via `IOptions` 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. ### 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 API rules — non-negotiable - `GET /api/application` is **BLOCKED**. Only POST and DELETE exist. - All group endpoints are `/api/group`, never `/api/usergroup`. - Feature assignment is `POST /api/group/{id}/acl`, NOT `/features`. - Xibo paginates at 10 items by default. **Always pass `length=200`** and use `GetAllPagesAsync` for every list call. Missing this causes silent data truncation. - OAuth2 client secret is returned **ONCE** in the `POST /api/application` response. Capture it immediately — **it cannot be retrieved again**. #### Stripe webhooks — idempotency is mandatory - Every Stripe webhook handler must check `OrchestratorDbContext.StripeEvents` for the `stripe_event_id` before processing anything. - Insert the `StripeEvent` row first, then process the webhook. This is not optional — duplicate webhook delivery is guaranteed by Stripe. #### No AI autonomy in infrastructure actions - Never generate any endpoint or method that sends a message, makes an external call, or takes infrastructure action without an explicit operator-initiated `Job` record being created first. - All automated actions flow through the `ProvisioningWorker` job queue. ## Build and Test ```bash # Build dotnet build OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj # Run dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj # No test suite currently — no xUnit/NUnit projects ``` - .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/` ### Test coverage non-negotiables Unit tests are **required** for: - Evidence hashing and tamper detection - AI context assembly - Pattern detection ruleset engine - `AbbreviationService` uniqueness logic - Stripe webhook idempotency Integration tests **require** Testcontainers with a real PostgreSQL 16 instance — **no SQLite substitutions**. ## 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> 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. ### Naming - Customer abbreviation: exactly 3 lowercase letters (`^[a-z]{3}$`). - Stack name: `{abbrev}-cms-stack`. Service: `{abbrev}-web`. DB: `{abbrev}_cms_db`. - Secret names built via `AppConstants` helpers (e.g., `CustomerMysqlPasswordSecretName(abbrev)`). - `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. ### 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`.** 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. ### 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 --project OTSSignsOrchestrator.Core --startup-project OTSSignsOrchestrator.Desktop` ### 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. ``` ## Pitfalls - **SSH singleton 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.