Files
OTSSignsOrchestrator/.github/copilot-instructions.md
Matt Batchelder c6d46098dd 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.
2026-03-18 10:27:26 -04:00

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 in App.axaml.csConfigureServices())
  • Singletons: stateful services (SSH connections, Docker CLI). 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.

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

# 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<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:

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<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:

// 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.