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.
This commit is contained in:
79
.github/copilot-instructions.md
vendored
79
.github/copilot-instructions.md
vendored
@@ -2,27 +2,21 @@
|
||||
|
||||
## Architecture
|
||||
|
||||
Layered MVVM desktop app for provisioning and managing Xibo CMS instances on Docker Swarm.
|
||||
Web-based system 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.
|
||||
- **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 `IServiceProvider` (registered in `App.axaml.cs` → `ConfigureServices()`)
|
||||
- Singletons: stateful services (SSH connections, Docker CLI). Transient: stateless logic.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
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.
|
||||
@@ -43,17 +37,20 @@ Xibo CMS REST API (OAuth2), Authentik SAML IdP, Bitwarden Secrets SDK, Docker Sw
|
||||
|
||||
```bash
|
||||
# Build
|
||||
dotnet build OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
|
||||
dotnet build
|
||||
|
||||
# Run
|
||||
dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
|
||||
# Run Server
|
||||
dotnet run --project OTSSignsOrchestrator/OTSSignsOrchestrator.csproj
|
||||
|
||||
# No test suite currently — no xUnit/NUnit projects
|
||||
# Run tests
|
||||
dotnet test OTSSignsOrchestrator.Tests/OTSSignsOrchestrator.Tests.csproj
|
||||
|
||||
# Frontend dev
|
||||
cd OTSSignsOrchestrator/ClientApp && npm run dev
|
||||
```
|
||||
|
||||
- .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/`
|
||||
- .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:
|
||||
@@ -67,28 +64,10 @@ Integration tests **require** Testcontainers with a real PostgreSQL 16 instance
|
||||
|
||||
## 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:
|
||||
```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).
|
||||
- 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.
|
||||
- `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}$`).
|
||||
@@ -97,27 +76,23 @@ Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { ... });
|
||||
- `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.
|
||||
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 that the step requires external infrastructure access that cannot be completed in this context.
|
||||
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/`, 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`
|
||||
- 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. Add an explicit comment on each repository class:
|
||||
```csharp
|
||||
// IMMUTABLE — no update or delete operations permitted.
|
||||
```
|
||||
**AuditLog, Message, and Evidence are append-only by design.** Never generate `Update()` or `Delete()` methods on these repositories.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **SSH singleton state**: `SshDockerCliService.SetHost()` must be called before each host operation — stale host causes silent failures.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user