mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-06-11 18:03:08 +08:00
ekg stats: add 6h/24h/7d window filter and webui selector for time-windowed insights
This commit is contained in:
@@ -1639,6 +1639,21 @@ func (s *RegistryServer) handleWebUIEKGStats(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
workspace := strings.TrimSpace(s.workspacePath)
|
workspace := strings.TrimSpace(s.workspacePath)
|
||||||
ekgPath := filepath.Join(workspace, "memory", "ekg-events.jsonl")
|
ekgPath := filepath.Join(workspace, "memory", "ekg-events.jsonl")
|
||||||
|
window := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("window")))
|
||||||
|
windowDur := 24 * time.Hour
|
||||||
|
switch window {
|
||||||
|
case "6h":
|
||||||
|
windowDur = 6 * time.Hour
|
||||||
|
case "24h", "":
|
||||||
|
windowDur = 24 * time.Hour
|
||||||
|
case "7d":
|
||||||
|
windowDur = 7 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
selectedWindow := window
|
||||||
|
if selectedWindow == "" {
|
||||||
|
selectedWindow = "24h"
|
||||||
|
}
|
||||||
|
cutoff := time.Now().UTC().Add(-windowDur)
|
||||||
b, _ := os.ReadFile(ekgPath)
|
b, _ := os.ReadFile(ekgPath)
|
||||||
lines := strings.Split(string(b), "\n")
|
lines := strings.Split(string(b), "\n")
|
||||||
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||||
@@ -1667,6 +1682,14 @@ func (s *RegistryServer) handleWebUIEKGStats(w http.ResponseWriter, r *http.Requ
|
|||||||
if json.Unmarshal([]byte(ln), &row) != nil {
|
if json.Unmarshal([]byte(ln), &row) != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
ts := strings.TrimSpace(fmt.Sprintf("%v", row["time"]))
|
||||||
|
if ts != "" {
|
||||||
|
if tm, err := time.Parse(time.RFC3339, ts); err == nil {
|
||||||
|
if tm.Before(cutoff) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
provider := strings.TrimSpace(fmt.Sprintf("%v", row["provider"]))
|
provider := strings.TrimSpace(fmt.Sprintf("%v", row["provider"]))
|
||||||
status := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", row["status"])))
|
status := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", row["status"])))
|
||||||
errSig := strings.TrimSpace(fmt.Sprintf("%v", row["errsig"]))
|
errSig := strings.TrimSpace(fmt.Sprintf("%v", row["errsig"]))
|
||||||
@@ -1731,6 +1754,7 @@ func (s *RegistryServer) handleWebUIEKGStats(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
"window": selectedWindow,
|
||||||
"provider_top": toTopScore(providerScore, 5),
|
"provider_top": toTopScore(providerScore, 5),
|
||||||
"provider_top_workload": toTopScore(providerScoreWorkload, 5),
|
"provider_top_workload": toTopScore(providerScoreWorkload, 5),
|
||||||
"errsig_top": toTopCount(errSigCount, 5),
|
"errsig_top": toTopCount(errSigCount, 5),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const TaskAudit: React.FC = () => {
|
|||||||
const [ekgSourceStats, setEkgSourceStats] = useState<Record<string, number>>({});
|
const [ekgSourceStats, setEkgSourceStats] = useState<Record<string, number>>({});
|
||||||
const [ekgChannelStats, setEkgChannelStats] = useState<Record<string, number>>({});
|
const [ekgChannelStats, setEkgChannelStats] = useState<Record<string, number>>({});
|
||||||
const [ekgEscalationCount, setEkgEscalationCount] = useState<number>(0);
|
const [ekgEscalationCount, setEkgEscalationCount] = useState<number>(0);
|
||||||
|
const [ekgWindow, setEkgWindow] = useState<'6h' | '24h' | '7d'>('24h');
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -67,7 +68,8 @@ const TaskAudit: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
setDailyReport('');
|
setDailyReport('');
|
||||||
}
|
}
|
||||||
const ekgUrl = `/webui/api/ekg_stats${q || ''}`;
|
const ekgJoin = q ? `${q}&window=${encodeURIComponent(ekgWindow)}` : `?window=${encodeURIComponent(ekgWindow)}`;
|
||||||
|
const ekgUrl = `/webui/api/ekg_stats${ekgJoin}`;
|
||||||
const er = await fetch(ekgUrl);
|
const er = await fetch(ekgUrl);
|
||||||
if (er.ok) {
|
if (er.ok) {
|
||||||
const ej = await er.json();
|
const ej = await er.json();
|
||||||
@@ -89,7 +91,7 @@ const TaskAudit: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [q, reportDate]);
|
useEffect(() => { fetchData(); }, [q, reportDate, ekgWindow]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +151,14 @@ const TaskAudit: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-3 text-sm">
|
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-3 text-sm">
|
||||||
<div className="text-xs text-zinc-400 uppercase tracking-wider mb-2">EKG Insights</div>
|
<div className="flex items-center justify-between gap-2 mb-2">
|
||||||
|
<div className="text-xs text-zinc-400 uppercase tracking-wider">EKG Insights</div>
|
||||||
|
<select value={ekgWindow} onChange={(e)=>setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs">
|
||||||
|
<option value="6h">6h</option>
|
||||||
|
<option value="24h">24h</option>
|
||||||
|
<option value="7d">7d</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
||||||
<div className="rounded-lg border border-zinc-800 bg-zinc-950/40 p-2">
|
<div className="rounded-lg border border-zinc-800 bg-zinc-950/40 p-2">
|
||||||
<div className="text-zinc-500 mb-1">Escalations</div>
|
<div className="text-zinc-500 mb-1">Escalations</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user