Files
OTSSignsOrchestrator/.github/copilot-instructions.md
Matt Batchelder 9a35e40083 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.
2026-03-23 21:28:14 -04:00

5.3 KiB

Project Guidelines — OTS Signs Orchestrator

Architecture

Web-based system for provisioning and managing Xibo CMS instances on Docker Swarm.

  • 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 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.
  • 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, Stripe, SendGrid.

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

# Run Server
dotnet run --project OTSSignsOrchestrator/OTSSignsOrchestrator.csproj

# Run tests
dotnet test OTSSignsOrchestrator.Tests/OTSSignsOrchestrator.Tests.csproj

# Frontend dev
cd OTSSignsOrchestrator/ClientApp && npm run dev
  • .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:

  • 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

Services

  • Interface + implementation pattern for testable services (IXiboApiService, IDockerCliService, etc.).
  • 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}$).
  • 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.

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.

Data layer

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

Pitfalls

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