mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Document state summary integration contract
This commit is contained in:
parent
e103278885
commit
73208a2863
5 changed files with 77 additions and 0 deletions
21
docs/API.md
21
docs/API.md
|
|
@ -68,6 +68,27 @@ Check if Pulse is running.
|
|||
`GET /api/state`
|
||||
Returns the complete state of your infrastructure (Nodes, VMs, Containers, Storage, Alerts). This is the main endpoint used by the dashboard.
|
||||
|
||||
`GET /api/state/summary`
|
||||
Returns a lightweight integration summary for external dashboards and checks. Requires `monitoring:read`.
|
||||
|
||||
```json
|
||||
{
|
||||
"activeAlerts": 1,
|
||||
"nodes": 2,
|
||||
"vms": 8,
|
||||
"containers": 12,
|
||||
"dockerHosts": [
|
||||
{
|
||||
"name": "Docker Host",
|
||||
"containers": 5,
|
||||
"uptimeSeconds": 86400,
|
||||
"cpuUsagePercent": 12.5
|
||||
}
|
||||
],
|
||||
"lastUpdate": "2026-05-24T10:11:12Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Stats (HTML)
|
||||
`GET /simple-stats`
|
||||
Lightweight HTML status page for quick checks.
|
||||
|
|
|
|||
|
|
@ -1772,6 +1772,13 @@ a new API state machine, queue contract, or verification-accounting field.
|
|||
page-local activity stores or a second query vocabulary.
|
||||
7. Route unified-resource list ordering through `internal/api/resources.go`, `internal/api/contract_test.go`, and the owned unified-resource registry helpers together; list payloads must stay deterministic for equal-name resources by carrying one canonical `name -> type -> id` tie-break across cold seed, REST pagination, and websocket-backed refreshes instead of inheriting map order or page-local re-sorts
|
||||
That same shared API contract also owns the external resource `type`, canonical display name, and cluster identity published through `/api/resources` and `/api/state`; the websocket/state hydrate path must not emit legacy aliases or raw store labels once the unified resource contract has normalized them.
|
||||
Lightweight integration polling uses the same canonical read-state boundary:
|
||||
`GET /api/state/summary` is an authenticated `monitoring:read` route that
|
||||
returns only aggregate counts, Docker host summaries, active-alert count,
|
||||
and `lastUpdate`. It must stay derived from the unified read state plus the
|
||||
alert snapshot, and must not expose `resources`, per-resource histories,
|
||||
raw provider payloads, command data, or child arrays from the full
|
||||
`/api/state` dashboard hydrate.
|
||||
Realtime `/api/state` and websocket `resources` snapshots must also collapse
|
||||
transient split host identities before broadcast: a Proxmox-node row and a
|
||||
host-agent row for the same machine must leave the API boundary as one
|
||||
|
|
|
|||
|
|
@ -532,6 +532,11 @@ dashboard-specific overview, trend, or summary transport.
|
|||
`/api/state` and paying an avoidable `401`; once a local-login hint exists
|
||||
or the operator is on a protected route, that shared state probe remains
|
||||
the canonical runtime detector.
|
||||
External dashboards and scripts that only need fleet counters must use
|
||||
`/api/state/summary`, not the full `/api/state` hydrate. That summary route
|
||||
must remain bounded to aggregate counts and Docker host rollups, with no
|
||||
resource arrays, metrics history, connected-infrastructure list, or chart
|
||||
payloads added to the response.
|
||||
That same app-shell performance boundary treats global commercial banners as
|
||||
non-hot-path UI. Retired self-hosted trial or managed-model acquisition
|
||||
surfaces must not be reintroduced into `frontend-modern/src/App.tsx` as
|
||||
|
|
|
|||
|
|
@ -162,6 +162,19 @@ func TestRouter_HandleStateSummary(t *testing.T) {
|
|||
assert.Equal(t, int64(3600), summary.DockerHosts[0].UptimeSeconds)
|
||||
assert.Equal(t, 12.5, summary.DockerHosts[0].CPUUsagePercent)
|
||||
}
|
||||
|
||||
if rec.Body.Len() >= 5*1024 {
|
||||
t.Fatalf("expected /api/state/summary body under 5 KiB, got %d bytes", rec.Body.Len())
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &payload)
|
||||
assert.NoError(t, err)
|
||||
for _, key := range []string{"resources", "metrics", "performance", "stats", "connectedInfrastructure", "pveTagStyles", "pveTagColors"} {
|
||||
if _, ok := payload[key]; ok {
|
||||
t.Fatalf("expected %q to be omitted from /api/state/summary payload", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildStateSummaryCountsSnapshotResources(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -4510,6 +4510,37 @@ func TestStateRequiresMonitoringReadScope(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStateSummaryRequiresAuthInAPIMode(t *testing.T) {
|
||||
record := newTokenRecord(t, "state-summary-token-123.12345678", []string{config.ScopeMonitoringRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/state/summary", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 without auth, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateSummaryRequiresMonitoringReadScope(t *testing.T) {
|
||||
rawToken := "state-summary-scope-token-123.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/state/summary", nil)
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 for missing monitoring:read scope, got %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringRead) {
|
||||
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringRead, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTemperatureSSHAllowsSetupToken(t *testing.T) {
|
||||
cfg := newTestConfigWithTokens(t)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue