The host-level remediation + security monitoring capability (#287) is live, but its output is not browsable in-app. Today the only ways to see what the swarm is reporting are:
Email to team@spikersoft.com (alert-on-threshold only — MinSeverity=high, with cooldowns, so most signal never produces a message).
Seq (the security console) for SecurityAlert log queries.
MongoDBsystem_incidents collection for the SystemIncident read-model — but nothing renders it.
There is no SpikerSoft.Api endpoint and no Angular page for any of this (confirmed: none of SystemIncident / SecurityAlert / HostJournalEvent are referenced in SpikerSoft.Api). Operators can't answer "what's unhealthy right now / what fired overnight" without shelling into Seq/Mongo.
Proposal
A read-only Ops console built in the existing idiom:
Backend (spikersoft-backend)
New read service + controller (e.g. api/ops/incidents, api/ops/incidents/{id}, summary/count endpoints) over the existing system_incidents Mongo read-model (MongoIncidentStore). Filter by node, kind, status (Detected/Remediating/Resolved/AlertedOnly), severity, time range.
Security alerts — DECIDED: persist to Mongo. Add a lightweight security_alerts read-model written by SpikerSoft.EventHandlers.SecurityMonitor (it currently only logs SecurityAlerts to Seq + keeps detector state in Redis; it has no Mongo dependency today). Persist alongside the existing Seq log + security.alert.{node} republish — do not remove those. Expose it via api/ops/security-alerts with node/category/severity/time filters. Mirror the MongoIncidentStore conventions (deterministic IDs / upsert + occurrence counter where it makes sense, configurable collection name, fail-open so a Mongo blip never blocks ingestion). This adds a MongoDB dependency to the SecurityMonitor stack — wire it into its docker-stack.yml (it already runs on SERVER, where Mongo is reachable) and health checks.
Read-only for v1: no remediation-command dispatch from the UI (the write/actuation path stays server-side, behind its existing toggles).
Frontend (spikersoft-angular)
New page (under /devops or /admin/ops) listing incidents + security alerts with the filters above and a detail view (occurrence count, signals, status timeline, last remediation result).
Menu entry surfaced only to authorized roles.
Authorization (hard requirement)
This surface must require the dev-ops OR admin role. staff is NOT sufficient — unlike the existing /admin/* tools which use the IsAdminOrStaff() / roles: ["Admin","Staff"] pattern.
Backend: add a stricter check/policy (e.g. IsDevOpsOrAdmin() or a named "DevOpsOrAdmin" authorization policy) and apply it to the new controller — do not reuse IsAdminOrStaff().
Frontend: guard the route with RoleGuard + data: { roles: ["dev-ops", "admin", "Admin"] } and gate the menu entry with hasAnyRole(...).
Dependency: the dev-ops realm role does not exist yet, and the role taxonomy is currently ad-hoc. This depends on the staff-role specialization work (#298). Until dev-ops exists, gate to admin only and wire dev-ops in once the role is defined.
Acceptance criteria
Read-only API over system_incidents with node/kind/status/severity/time filters.
SecurityMonitor persists a security_alerts read-model to Mongo (in addition to Seq), exposed via a read-only api/ops/security-alerts with node/category/severity/time filters.
Mongo persistence in SecurityMonitor is fail-open and its stack/health checks updated for the new MongoDB dependency.
Angular ops page (list + detail) consuming both feeds.
Access denied (403 / redirect) for staff-only and unauthenticated users; allowed for dev-ops and admin.
No remediation actions are dispatchable from the UI in v1.
Backend authorization does not fall back to IsAdminOrStaff().
Out of scope (v1)
Triggering/approving remediation commands from the UI.
Live streaming/websocket updates (poll/refresh is fine to start).
Backfilling historical alerts that only ever hit Seq (the Mongo read-model starts accumulating from deploy).
## Problem
The host-level remediation + security monitoring capability (#287) is live, but its output is **not browsable in-app**. Today the only ways to see what the swarm is reporting are:
- **Email** to `team@spikersoft.com` (alert-on-threshold only — `MinSeverity=high`, with cooldowns, so most signal never produces a message).
- **Seq** (the security console) for `SecurityAlert` log queries.
- **MongoDB** `system_incidents` collection for the `SystemIncident` read-model — but nothing renders it.
There is **no `SpikerSoft.Api` endpoint and no Angular page** for any of this (confirmed: none of `SystemIncident` / `SecurityAlert` / `HostJournalEvent` are referenced in `SpikerSoft.Api`). Operators can't answer "what's unhealthy right now / what fired overnight" without shelling into Seq/Mongo.
## Proposal
A read-only **Ops console** built in the existing idiom:
**Backend (`spikersoft-backend`)**
- New read service + controller (e.g. `api/ops/incidents`, `api/ops/incidents/{id}`, summary/count endpoints) over the existing `system_incidents` Mongo read-model (`MongoIncidentStore`). Filter by node, kind, status (`Detected/Remediating/Resolved/AlertedOnly`), severity, time range.
- **Security alerts — DECIDED: persist to Mongo.** Add a lightweight `security_alerts` read-model written by `SpikerSoft.EventHandlers.SecurityMonitor` (it currently only logs `SecurityAlert`s to Seq + keeps detector state in Redis; it has **no** Mongo dependency today). Persist alongside the existing Seq log + `security.alert.{node}` republish — do not remove those. Expose it via `api/ops/security-alerts` with node/category/severity/time filters. Mirror the `MongoIncidentStore` conventions (deterministic IDs / upsert + occurrence counter where it makes sense, configurable collection name, fail-open so a Mongo blip never blocks ingestion). This adds a MongoDB dependency to the SecurityMonitor stack — wire it into its `docker-stack.yml` (it already runs on `SERVER`, where Mongo is reachable) and health checks.
- Read-only for v1: **no** remediation-command dispatch from the UI (the write/actuation path stays server-side, behind its existing toggles).
**Frontend (`spikersoft-angular`)**
- New page (under `/devops` or `/admin/ops`) listing incidents + security alerts with the filters above and a detail view (occurrence count, signals, status timeline, last remediation result).
- Menu entry surfaced only to authorized roles.
## Authorization (hard requirement)
This surface must require the **`dev-ops`** OR **`admin`** role. **`staff` is NOT sufficient** — unlike the existing `/admin/*` tools which use the `IsAdminOrStaff()` / `roles: ["Admin","Staff"]` pattern.
- Backend: add a stricter check/policy (e.g. `IsDevOpsOrAdmin()` or a named `"DevOpsOrAdmin"` authorization policy) and apply it to the new controller — do **not** reuse `IsAdminOrStaff()`.
- Frontend: guard the route with `RoleGuard` + `data: { roles: ["dev-ops", "admin", "Admin"] }` and gate the menu entry with `hasAnyRole(...)`.
**Dependency:** the `dev-ops` realm role does not exist yet, and the role taxonomy is currently ad-hoc. This depends on the staff-role specialization work (#298). Until `dev-ops` exists, gate to `admin` only and wire `dev-ops` in once the role is defined.
## Acceptance criteria
- [ ] Read-only API over `system_incidents` with node/kind/status/severity/time filters.
- [ ] `SecurityMonitor` persists a `security_alerts` read-model to Mongo (in addition to Seq), exposed via a read-only `api/ops/security-alerts` with node/category/severity/time filters.
- [ ] Mongo persistence in `SecurityMonitor` is fail-open and its stack/health checks updated for the new MongoDB dependency.
- [ ] Angular ops page (list + detail) consuming both feeds.
- [ ] Access denied (403 / redirect) for `staff`-only and unauthenticated users; allowed for `dev-ops` and `admin`.
- [ ] No remediation actions are dispatchable from the UI in v1.
- [ ] Backend authorization does not fall back to `IsAdminOrStaff()`.
## Out of scope (v1)
- Triggering/approving remediation commands from the UI.
- Live streaming/websocket updates (poll/refresh is fine to start).
- Backfilling historical alerts that only ever hit Seq (the Mongo read-model starts accumulating from deploy).
Related: #287 (capability), #298 (staff-role specialization / `dev-ops` role).
Companion / dependency: #298 (specialize the keycloak-admin library toward SpikerSoft staff roles and define the dev-ops role). Until dev-ops exists, gate this console to admin only and add dev-ops once #298 lands.
Companion / dependency: #298 (specialize the `keycloak-admin` library toward SpikerSoft staff roles and define the `dev-ops` role). Until `dev-ops` exists, gate this console to `admin` only and add `dev-ops` once #298 lands.
Decision locked: persist security alerts to Mongo (option a). SecurityMonitor will write a fail-open security_alerts read-model (mirroring MongoIncidentStore conventions) in addition to its existing Seq log + RabbitMQ republish, and the ops API will read from it via api/ops/security-alerts. This adds a MongoDB dependency to the SecurityMonitor stack. Updated the body and acceptance criteria accordingly; the Seq-proxy alternative is dropped.
**Decision locked: persist security alerts to Mongo** (option a). `SecurityMonitor` will write a fail-open `security_alerts` read-model (mirroring `MongoIncidentStore` conventions) in addition to its existing Seq log + RabbitMQ republish, and the ops API will read from it via `api/ops/security-alerts`. This adds a MongoDB dependency to the SecurityMonitor stack. Updated the body and acceptance criteria accordingly; the Seq-proxy alternative is dropped.
Deliverable 1 (read-only incidents API) merged in spikersoft-backend PR #93. api/ops/incidents (paged list, filters node/kind/status/severity/time), .../{id} (detail), .../summary (counts by status/severity/node) over the existing system_incidents read-model — so the auto-remediation incidents (#376) are now browsable via API instead of Seq/Mongo-only.
No heavy-dep leak: the API doesn't reference the worker Infrastructure assembly where SystemIncident lives; added a Bson read DTO + raw-IMongoClient read service (fail-open), reusing the writer's Remediation:IncidentsCollection.
Authz per the hard requirement:OpsAuthorization.IsDevOpsOrAdmin — admin-only, does NOT reuse IsAdminOrStaff() (staff denied, proven by tests). One-line seam to add the dev-ops role once #298 defines it — marked // #298.
31 new tests (staff/anon/plain-user denied across all three endpoints; admin allowed; filters/paging/sort/summary).
Deliverable 2 — SecurityMonitor persists security_alerts to Mongo + api/ops/security-alerts. Deferred because it adds a MongoDB dependency to that worker + its docker-stack.yml + health checks (has an infra-repo change).
Angular ops page (list + detail, dev-ops/admin route guard) — separate angular PR.
**Deliverable 1 (read-only incidents API) merged** in spikersoft-backend PR #93. `api/ops/incidents` (paged list, filters node/kind/status/severity/time), `.../{id}` (detail), `.../summary` (counts by status/severity/node) over the existing `system_incidents` read-model — so the auto-remediation incidents (#376) are now browsable via API instead of Seq/Mongo-only.
- **No heavy-dep leak:** the API doesn't reference the worker `Infrastructure` assembly where `SystemIncident` lives; added a Bson read DTO + raw-`IMongoClient` read service (fail-open), reusing the writer's `Remediation:IncidentsCollection`.
- **Authz per the hard requirement:** `OpsAuthorization.IsDevOpsOrAdmin` — **admin-only**, does NOT reuse `IsAdminOrStaff()` (staff denied, proven by tests). One-line seam to add the `dev-ops` role once #298 defines it — marked `// #298`.
- 31 new tests (staff/anon/plain-user denied across all three endpoints; admin allowed; filters/paging/sort/summary).
**Still open for #297:**
- **Deliverable 2** — `SecurityMonitor` persists `security_alerts` to Mongo + `api/ops/security-alerts`. Deferred because it adds a MongoDB dependency to that worker + its `docker-stack.yml` + health checks (has an infra-repo change).
- **Angular ops page** (list + detail, `dev-ops`/admin route guard) — separate angular PR.
- Wire the real `dev-ops` role in once #298 lands.
Ticket stays open.
Deliverable 2 staged as spikersoft-backend PR #95 — NOT merged, needs your call (flagging a latent prod-safety issue it uncovered).
⚠️Discovery: the write-side already landed on master as commit bfbff89 (Cursor-authored, Jul 1) ungated / always-on — it unconditionally does .WithMongoDB(), adds a Mongo health check to /healthz, and always persists. But spikersoft-security-monitor/docker-stack.yml has no mongo network (only rabbitmq/redis/seq-attachable/jaeger). So a deploy from that commit creates an IMongoClient that can't reach mongo-router:27017 (connection-failure noise) and its Mongo health check fails → /healthz unhealthy → swarm restart risk. Worth checking whether SecurityMonitor has been flapping since Jul 1 (its healthz is VPN-internal on SERVER:8094, so I can't reach it from here).
Fix: flag-gates the write-side OFF by default (SecurityMonitor:PersistAlertsToMongo) — OFF ⇒ no Mongo client / health check / persistence, byte-identical to pre-bfbff89 (matches the actual infra). Merging + deploying this returns SecurityMonitor to a safe state.
Feature: adds the read-only api/ops/security-alerts endpoint (admin-gated, mirrors the #93 incidents API), fail-open to empty until data exists.
Your decision (two valid paths):
(a) Accept default-off now (fixes the risk immediately) → later add the mongo network to the SecurityMonitor stack + flip PersistAlertsToMongo=true to enable persistence.
(b) Fix-forward → add the mongo network first, then this can default on.
I left it unmerged because it reverses bfbff89's always-on behavior, and that's your architectural call (plus you may want eyes on how an always-on Mongo dep reached a worker with no mongo network). 113/113 filtered tests green; off-path proven to create no Mongo client. Ticket stays open.
**Deliverable 2 staged as spikersoft-backend PR #95 — NOT merged, needs your call** (flagging a latent prod-safety issue it uncovered).
⚠️ **Discovery:** the write-side already landed on master as commit `bfbff89` (Cursor-authored, Jul 1) **ungated / always-on** — it unconditionally does `.WithMongoDB()`, adds a Mongo health check to `/healthz`, and always persists. But `spikersoft-security-monitor/docker-stack.yml` has **no `mongo` network** (only rabbitmq/redis/seq-attachable/jaeger). So a deploy from that commit creates an `IMongoClient` that can't reach `mongo-router:27017` (connection-failure noise) **and its Mongo health check fails → `/healthz` unhealthy → swarm restart risk.** Worth checking whether SecurityMonitor has been flapping since Jul 1 (its healthz is VPN-internal on SERVER:8094, so I can't reach it from here).
**PR #95 does two things:**
1. **Fix:** flag-gates the write-side OFF by default (`SecurityMonitor:PersistAlertsToMongo`) — OFF ⇒ no Mongo client / health check / persistence, byte-identical to pre-`bfbff89` (matches the actual infra). Merging + deploying this returns SecurityMonitor to a safe state.
2. **Feature:** adds the read-only `api/ops/security-alerts` endpoint (admin-gated, mirrors the #93 incidents API), fail-open to empty until data exists.
**Your decision (two valid paths):**
- **(a) Accept default-off now** (fixes the risk immediately) → later add the `mongo` network to the SecurityMonitor stack + flip `PersistAlertsToMongo=true` to enable persistence.
- **(b) Fix-forward** → add the `mongo` network first, then this can default on.
I left it unmerged because it reverses `bfbff89`'s always-on behavior, and that's your architectural call (plus you may want eyes on how an always-on Mongo dep reached a worker with no mongo network). 113/113 filtered tests green; off-path proven to create no Mongo client. Ticket stays open.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The host-level remediation + security monitoring capability (#287) is live, but its output is not browsable in-app. Today the only ways to see what the swarm is reporting are:
team@spikersoft.com(alert-on-threshold only —MinSeverity=high, with cooldowns, so most signal never produces a message).SecurityAlertlog queries.system_incidentscollection for theSystemIncidentread-model — but nothing renders it.There is no
SpikerSoft.Apiendpoint and no Angular page for any of this (confirmed: none ofSystemIncident/SecurityAlert/HostJournalEventare referenced inSpikerSoft.Api). Operators can't answer "what's unhealthy right now / what fired overnight" without shelling into Seq/Mongo.Proposal
A read-only Ops console built in the existing idiom:
Backend (
spikersoft-backend)api/ops/incidents,api/ops/incidents/{id}, summary/count endpoints) over the existingsystem_incidentsMongo read-model (MongoIncidentStore). Filter by node, kind, status (Detected/Remediating/Resolved/AlertedOnly), severity, time range.security_alertsread-model written bySpikerSoft.EventHandlers.SecurityMonitor(it currently only logsSecurityAlerts to Seq + keeps detector state in Redis; it has no Mongo dependency today). Persist alongside the existing Seq log +security.alert.{node}republish — do not remove those. Expose it viaapi/ops/security-alertswith node/category/severity/time filters. Mirror theMongoIncidentStoreconventions (deterministic IDs / upsert + occurrence counter where it makes sense, configurable collection name, fail-open so a Mongo blip never blocks ingestion). This adds a MongoDB dependency to the SecurityMonitor stack — wire it into itsdocker-stack.yml(it already runs onSERVER, where Mongo is reachable) and health checks.Frontend (
spikersoft-angular)/devopsor/admin/ops) listing incidents + security alerts with the filters above and a detail view (occurrence count, signals, status timeline, last remediation result).Authorization (hard requirement)
This surface must require the
dev-opsORadminrole.staffis NOT sufficient — unlike the existing/admin/*tools which use theIsAdminOrStaff()/roles: ["Admin","Staff"]pattern.IsDevOpsOrAdmin()or a named"DevOpsOrAdmin"authorization policy) and apply it to the new controller — do not reuseIsAdminOrStaff().RoleGuard+data: { roles: ["dev-ops", "admin", "Admin"] }and gate the menu entry withhasAnyRole(...).Dependency: the
dev-opsrealm role does not exist yet, and the role taxonomy is currently ad-hoc. This depends on the staff-role specialization work (#298). Untildev-opsexists, gate toadminonly and wiredev-opsin once the role is defined.Acceptance criteria
system_incidentswith node/kind/status/severity/time filters.SecurityMonitorpersists asecurity_alertsread-model to Mongo (in addition to Seq), exposed via a read-onlyapi/ops/security-alertswith node/category/severity/time filters.SecurityMonitoris fail-open and its stack/health checks updated for the new MongoDB dependency.staff-only and unauthenticated users; allowed fordev-opsandadmin.IsAdminOrStaff().Out of scope (v1)
Related: #287 (capability), #298 (staff-role specialization /
dev-opsrole).Companion / dependency: #298 (specialize the
keycloak-adminlibrary toward SpikerSoft staff roles and define thedev-opsrole). Untildev-opsexists, gate this console toadminonly and adddev-opsonce #298 lands.Decision locked: persist security alerts to Mongo (option a).
SecurityMonitorwill write a fail-opensecurity_alertsread-model (mirroringMongoIncidentStoreconventions) in addition to its existing Seq log + RabbitMQ republish, and the ops API will read from it viaapi/ops/security-alerts. This adds a MongoDB dependency to the SecurityMonitor stack. Updated the body and acceptance criteria accordingly; the Seq-proxy alternative is dropped.Deliverable 1 (read-only incidents API) merged in spikersoft-backend PR #93.
api/ops/incidents(paged list, filters node/kind/status/severity/time),.../{id}(detail),.../summary(counts by status/severity/node) over the existingsystem_incidentsread-model — so the auto-remediation incidents (#376) are now browsable via API instead of Seq/Mongo-only.Infrastructureassembly whereSystemIncidentlives; added a Bson read DTO + raw-IMongoClientread service (fail-open), reusing the writer'sRemediation:IncidentsCollection.OpsAuthorization.IsDevOpsOrAdmin— admin-only, does NOT reuseIsAdminOrStaff()(staff denied, proven by tests). One-line seam to add thedev-opsrole once #298 defines it — marked// #298.Still open for #297:
SecurityMonitorpersistssecurity_alertsto Mongo +api/ops/security-alerts. Deferred because it adds a MongoDB dependency to that worker + itsdocker-stack.yml+ health checks (has an infra-repo change).dev-ops/admin route guard) — separate angular PR.dev-opsrole in once #298 lands.Ticket stays open.
Deliverable 2 staged as spikersoft-backend PR #95 — NOT merged, needs your call (flagging a latent prod-safety issue it uncovered).
⚠️ Discovery: the write-side already landed on master as commit
bfbff89(Cursor-authored, Jul 1) ungated / always-on — it unconditionally does.WithMongoDB(), adds a Mongo health check to/healthz, and always persists. Butspikersoft-security-monitor/docker-stack.ymlhas nomongonetwork (only rabbitmq/redis/seq-attachable/jaeger). So a deploy from that commit creates anIMongoClientthat can't reachmongo-router:27017(connection-failure noise) and its Mongo health check fails →/healthzunhealthy → swarm restart risk. Worth checking whether SecurityMonitor has been flapping since Jul 1 (its healthz is VPN-internal on SERVER:8094, so I can't reach it from here).PR #95 does two things:
SecurityMonitor:PersistAlertsToMongo) — OFF ⇒ no Mongo client / health check / persistence, byte-identical to pre-bfbff89(matches the actual infra). Merging + deploying this returns SecurityMonitor to a safe state.api/ops/security-alertsendpoint (admin-gated, mirrors the #93 incidents API), fail-open to empty until data exists.Your decision (two valid paths):
mongonetwork to the SecurityMonitor stack + flipPersistAlertsToMongo=trueto enable persistence.mongonetwork first, then this can default on.I left it unmerged because it reverses
bfbff89's always-on behavior, and that's your architectural call (plus you may want eyes on how an always-on Mongo dep reached a worker with no mongo network). 113/113 filtered tests green; off-path proven to create no Mongo client. Ticket stays open.Board-sweep status (2026-07-22): backend fully merged (PR #48 write side, #93 incidents read API, #95 flag-gated persistence + security-alerts API). REMAINING: the Angular ops-console page — no trace on angular master.