feat: Implement provisioning pipelines for subscription management

- 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.
This commit is contained in:
Matt Batchelder
2026-03-18 10:27:26 -04:00
parent c2e03de8bb
commit c6d46098dd
77 changed files with 9412 additions and 29 deletions

View File

@@ -15,9 +15,30 @@ Layered MVVM desktop app for provisioning and managing Xibo CMS instances on Doc
- 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
@@ -34,6 +55,16 @@ dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.c
- 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
@@ -42,6 +73,13 @@ dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.c
- 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).
@@ -58,11 +96,23 @@ dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.c
- 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<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 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`
### 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.