kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
107 行
3.9 KiB
Go
107 行
3.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.kenn.io/agentsview/internal/activity"
|
|
"go.kenn.io/agentsview/internal/db"
|
|
)
|
|
|
|
func (s *Server) registerActivityRoutes() {
|
|
group := newRouteGroup(s.api, "/api/v1/activity", "Activity")
|
|
get(s, group, "/report", "Get activity report", s.humaActivityReport)
|
|
}
|
|
|
|
type activityReportInput struct {
|
|
Preset string `query:"preset" enum:"day,week,month,custom" doc:"Range preset"`
|
|
Date string `query:"date" format:"date" doc:"Calendar day (YYYY-MM-DD) for presets"`
|
|
From string `query:"from" doc:"Range start (RFC3339) for custom ranges"`
|
|
To string `query:"to" doc:"Range end (RFC3339) for custom ranges"`
|
|
Timezone string `query:"timezone" doc:"IANA timezone name"`
|
|
Bucket string `query:"bucket" enum:"5m,15m,1h,1d,1w" doc:"Timeline bucket size override"`
|
|
Project string `query:"project" doc:"Filter by project"`
|
|
GitBranch string `query:"git_branch" doc:"Filter by git branch; opaque (project, branch) tokens from the /branches endpoint"`
|
|
Agent string `query:"agent" doc:"Filter by agent"`
|
|
Machine string `query:"machine" doc:"Filter by machine"`
|
|
// Automation classes the report: "all" (default) keeps both, "interactive"
|
|
// drops automated sessions, "automated" drops interactive ones. Empty is
|
|
// treated as "all"; any other value is rejected.
|
|
Automation string `query:"automation" default:"all" doc:"Automation class: all, interactive, or automated"`
|
|
}
|
|
|
|
func (s *Server) humaActivityReport(
|
|
ctx context.Context, in *activityReportInput,
|
|
) (*jsonOutput[activity.Report], error) {
|
|
tz := in.Timezone
|
|
if tz == "" {
|
|
tz = "UTC"
|
|
}
|
|
loc, err := time.LoadLocation(tz)
|
|
if err != nil {
|
|
return nil, apiError(http.StatusBadRequest, "invalid timezone: "+tz)
|
|
}
|
|
input := activity.QueryInput{
|
|
Preset: in.Preset, Date: in.Date, From: in.From, To: in.To,
|
|
Timezone: tz, BucketOverride: in.Bucket,
|
|
}
|
|
// Presets need an anchor date; default to today in the requested
|
|
// timezone, matching the prior day-only handler's behavior.
|
|
if input.Date == "" && input.From == "" {
|
|
input.Date = time.Now().In(loc).Format("2006-01-02")
|
|
}
|
|
q, err := activity.ResolveQuery(input, time.Now())
|
|
if err != nil {
|
|
return nil, apiError(http.StatusBadRequest, err.Error())
|
|
}
|
|
excludeAutomated, excludeInteractive, err := activityAutomationFilter(in.Automation)
|
|
if err != nil {
|
|
return nil, apiError(http.StatusBadRequest, err.Error())
|
|
}
|
|
// The activity report intentionally includes one-shot sessions, unlike
|
|
// analytics which excludes them by default. The automation class is the
|
|
// caller's choice (default "all" keeps both automated and interactive).
|
|
f := db.AnalyticsFilter{
|
|
Timezone: tz, Project: in.Project, GitBranch: in.GitBranch,
|
|
Agent: in.Agent, Machine: in.Machine,
|
|
ExcludeOneShot: false,
|
|
ExcludeAutomated: excludeAutomated,
|
|
ExcludeInteractive: excludeInteractive,
|
|
}
|
|
r, err := s.db.GetActivityReport(ctx, f, q)
|
|
if err != nil {
|
|
if handled := handleHumaContextError(err); handled != nil {
|
|
return nil, handled
|
|
}
|
|
if handled := handleHumaReadOnly(err); handled != nil {
|
|
return nil, handled
|
|
}
|
|
return nil, internalError("activity report error", err)
|
|
}
|
|
return &jsonOutput[activity.Report]{Body: r}, nil
|
|
}
|
|
|
|
// activityAutomationFilter maps the activity report's automation query value to
|
|
// the AnalyticsFilter class exclusions. Empty and "all" keep both classes;
|
|
// "interactive" drops automated sessions; "automated" drops interactive ones.
|
|
// Any other value is an error so a typo surfaces as 400 rather than silently
|
|
// returning the unfiltered report.
|
|
func activityAutomationFilter(
|
|
automation string,
|
|
) (excludeAutomated, excludeInteractive bool, err error) {
|
|
switch automation {
|
|
case "", "all":
|
|
return false, false, nil
|
|
case "interactive":
|
|
return true, false, nil
|
|
case "automated":
|
|
return false, true, nil
|
|
default:
|
|
return false, false, fmt.Errorf(
|
|
"invalid automation %q (want all, interactive, or automated)",
|
|
automation)
|
|
}
|
|
}
|