Files
OrchestrAD/backend/internal/api/activity_handlers.go
Alphaeus Mote e74cb454c8 feat(activity): add action-ledger intelligence and drill-in feed
Turn the rule_run_actions ledger into operator intelligence. A new
ActivityService rolls the ledger up by time window (24h / 7d / all-time)
into syncs, operations and removals with success/failure counts and the
number of distinct objects affected, plus all-time totals per action type
and a most-active-rules ranking. A companion filtered, paginated feed
answers the operational questions directly: what action ran, what
happened, to which object, triggered by whom, and when.

Backend:
- ActivityService.Summary() and ListActions(filter) over the joined
  rule_run_actions / rule_runs / rules tables, categorising each action
  type as Sync (AddToGroup/AddGroupToGroup), Removal
  (RemoveFromGroupIfNoLongerMatched) or Operation (everything else).
- GET /api/v1/activity/summary and GET /api/v1/activity (category,
  actionType, status, ruleId, search, pagination).
- Table-driven test seeding a run with mixed action types/statuses and
  asserting window roll-ups, distinct-object counts, top rules, and the
  category/status feed filters.

Frontend:
- New Activity page: window summary cards, by-action-type and
  most-active-rules tables, and a drill-in feed with category/result/
  object filters and a detail dialog that pretty-prints the action's
  details JSON and error.
- Wired into the vertical sidebar and horizontal navbar under Automation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 14:09:04 -04:00

54 lines
1.7 KiB
Go

// Package api - activity intelligence handlers
package api
import (
"net/http"
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
"github.com/Grace-Solutions/OrchestrAD/internal/services"
)
// ActivityHandler serves aggregated action intelligence and the drill-in feed.
type ActivityHandler struct {
service *services.ActivityService
logger *logging.Logger
}
// NewActivityHandler creates a new ActivityHandler.
func NewActivityHandler(service *services.ActivityService, logger *logging.Logger) *ActivityHandler {
return &ActivityHandler{service: service, logger: logger}
}
// Summary handles GET /api/v1/activity/summary
func (h *ActivityHandler) Summary(w http.ResponseWriter, r *http.Request) {
summary, err := h.service.Summary()
if err != nil {
h.logger.Error("ActivityHandler", "Summary failed: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to compute activity summary")
return
}
WriteJSON(w, http.StatusOK, summary)
}
// List handles GET /api/v1/activity — the filtered, paginated action feed.
func (h *ActivityHandler) List(w http.ResponseWriter, r *http.Request) {
p := ParsePagination(r)
q := r.URL.Query()
filter := services.ActivityFilter{
Category: q.Get("category"),
ActionType: q.Get("actionType"),
Status: q.Get("status"),
RuleID: q.Get("ruleId"),
Search: q.Get("search"),
Offset: (p.Page - 1) * p.PageSize,
Limit: p.PageSize,
}
records, total, err := h.service.ListActions(filter)
if err != nil {
h.logger.Error("ActivityHandler", "List failed: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to list activity")
return
}
WriteList(w, records, p.Page, p.PageSize, total)
}