- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes. - Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging. - Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR. - Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes. - Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots. - Introduce XiboFeatureManifests for hardcoded feature ACLs per role. - Add docker-compose.dev.yml for local development with PostgreSQL setup.
7.5 KiB
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 inApp.axaml.cs→ConfigureServices()) - Singletons: stateful services (SSH connections, Docker CLI). Transient: stateless logic.
- Configuration via
IOptions<T>bound fromappsettings.json(seeAppOptions.csfor all sections). - Bitwarden Secrets Manager is the source of truth for all sensitive config.
SettingsServicecaches 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.CoreorOTSSignsOrchestrator.Desktopunless the prompt explicitly says to. - When in doubt, add new code to
OTSSignsOrchestrator.Server. - Never modify
XiboContext.cswithout 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/applicationis 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=200and useGetAllPagesAsyncfor every list call. Missing this causes silent data truncation. - OAuth2 client secret is returned ONCE in the
POST /api/applicationresponse. Capture it immediately — it cannot be retrieved again.
Stripe webhooks — idempotency is mandatory
- Every Stripe webhook handler must check
OrchestratorDbContext.StripeEventsfor thestripe_event_idbefore processing anything. - Insert the
StripeEventrow 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
Jobrecord being created first. - All automated actions flow through the
ProvisioningWorkerjob queue.
Build and Test
# 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
AbbreviationServiceuniqueness 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
IServiceProviderin constructors. Navigation viaMainWindowViewModel.CurrentView. - Confirmation dialogs use
Func<string, string, Task<bool>> ConfirmAsyncproperty — 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:
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 inMainWindow.axamlmap ViewModel types to View UserControls. - Layout: DockPanel with status bar (bottom), sidebar nav (left), dynamic ContentControl (center).
- Style: Fluent theme, dark palette (
#0C0C14accents).
Services
- Interface + implementation pattern for testable services (
IXiboApiService,IDockerCliService, etc.). SshDockerCliServiceis a singleton — must callSetHost(host)before each operation in loops.- All long operations are
async Task. UseIsBusy+StatusMessageproperties 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
AppConstantshelpers (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<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.
Data layer
- Entities in
Core/Models/Entities/, DTOs inCore/Models/DTOs/. XiboContextapplies unique index onSshHost.Labeland encrypts credential fields.- Add new migrations via:
dotnet ef migrations add <Name> --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:
// 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: trueto 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.