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

99 lines
5.3 KiB
Markdown

# 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
```bash
# 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.