- 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.
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 fromappsettings.json(seeAppOptions.csfor all sections). - Bitwarden Secrets Manager is the source of truth for all sensitive config.
SettingsServicecaches in-memory. - PostgreSQL database via
OrchestratorDbContext. Credentials encrypted via Data Protection API. - React frontend in
ClientApp/, built towwwroot/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/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
# 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
AbbreviationServiceuniqueness 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.). SshDockerCliServiceis scoped — must callSetHost(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
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.
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/andServer/Data/Entities/. - DTOs in
Core/Models/DTOs/. OrchestratorDbContextis 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: 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.