GitHub
07/01/2026, 9:01 AMpkg/activity) was added recently but was dead plumbing: osctrl-tls constructed an ActivityWriter and wired it onto the service handler, but no endpoint ever called addEvent, so the per-endpoint hourly rollups (enroll / config / status / result / query-read / query-write) were never written. The API never read the store either — the SPA's activity heatmaps are served from DB bucket queries — so the finer-grained series (the config fetches and the query read/write split that the DB buckets collapse into a single query category) were never displayed.
This wires the store end-to-end so the per-endpoint last-seen activity shows in the frontend.
Changes
osctrl-tls — feed the store
• Added a nil-safe, fire-and-forget recordActivity(envUUID, nodeUUID, type) helper on HandlersTLS plus logActivityType() to map osquery log types.
• Emit activity.Event at every osquery endpoint in `cmd/tls/handlers/post.go`:
• EnrollHandler → EventEnroll (guarded by !nodeInvalid)
• ConfigHandler → EventConfig
• LogHandler → EventStatus / EventResult via logActivityType (unknown log types are skipped, never recorded)
• QueryReadHandler → EventQueryRead
• QueryWriteHandler → EventQueryWrite
• Carve endpoints are intentionally excluded: there is no carve EventType, and inventing one would change the Redis bit layout / EventTypeCount.
osctrl-api — expose the series
• cmd/api already held a Redis client; added an *activity.RedisStore reader using the same prefix and retention as TLS so reads match writes.
• Two new additive endpoints, both behind the existing JWT auth + per-env UserLevel permission + node/env membership checks:
• GET /api/v1/stats/activity/node-tiles/{env}/{uuid}?days=N → activity.NodeTileSeries
• GET /api/v1/stats/activity/env-tiles/{env}?days=N → activity.EnvSeries
• ?days defaults to 1 and clamps to `DefaultRetentionDays`; returns 503 when the activity store is not configured (not 500).
• Existing DB-backed /stats/activity/... endpoints and their SPA consumers are unchanged.
Frontend — display per-endpoint last-seen
• `src/api/stats.ts`: NodeTileSeries type, getNodeActivityTiles / getEnvActivityTiles fetchers, and tileLastSeen / tileCategoryTotal helpers.
• `src/features/nodes/NodeDetailPage.tsx`: new "Endpoint activity · last 24h" panel under the existing heatmap, showing each of config / status / result / query-read / query-write with its event total and a relative last-seen timestamp. Last-seen granularity is hourly (the Redis rollup bucket size).
Behavior / risk notes
• The activity write path is non-blocking: addEvent drops on a full queue (existing behavior) and recordActivity no-ops on a nil writer or empty env/node UUIDs, so it can never fail or stall a TLS request.
• Security-sensitive TLS handlers are touched only additively (one fire-and-forget call each after the existing last-seen batch update); no request parsing, auth, or response logic changed.
• go.mod / go.sum are untouched.
Validation
• go build ./cmd/... — clean.
• go test ./cmd/tls/handlers/... (activity tests): TestRecordActivityEmitsTypedEvent, TestRecordActivityIsNoopWhenNotConfiguredOrMissingIDs, TestLogActivityTypeMapping — pass.
• go test ./cmd/api/handlers/... (tile tests): TestActivityTileDays, TestNodeTileSeriesJSONShape — pass.
• go test ./pkg/activity/... — pass.
• Frontend tsc --noEmit — clean; NodeDetailPage.test.tsx — pass (mock updated to stub the new tiles fetch).
Pre-existing failures unrelated to this change (reproduce without these edits, in untouched files): `LoginPage`/`ProfilePage` SPA tests fail on localStorage under jsdom, and the Go `auth_oidc`/`auth_logout` tests panic in httptest.NewServer under the sandbox network restriction.
Out of scope / follow-ups
• A carve activity type (and carve last-seen) would require a new EventType and a Redis blob layout change; deferred.
• The env-tiles endpoint is wired and tested but not yet rendered in the SPA dashboard; the node-tiles panel is the user-facing surface added here.
jmpsec/osctrlGitHub
07/01/2026, 12:04 PM