diff --git a/web/src/lib/api.analytics.test.ts b/web/src/lib/api.analytics.test.ts
new file mode 100644
index 000000000..78b5c0e4a
--- /dev/null
+++ b/web/src/lib/api.analytics.test.ts
@@ -0,0 +1,74 @@
+///
+
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { normalizeAnalyticsResponse, type AnalyticsResponse } from "./api.ts";
+
+test("normalizeAnalyticsResponse fills in missing skill analytics for older backends", () => {
+ const raw = {
+ daily: [],
+ by_model: [],
+ totals: {
+ total_input: 0,
+ total_output: 0,
+ total_cache_read: 0,
+ total_reasoning: 0,
+ total_estimated_cost: 0,
+ total_actual_cost: 0,
+ total_sessions: 0,
+ total_api_calls: 0,
+ },
+ } as AnalyticsResponse;
+
+ const normalized = normalizeAnalyticsResponse(raw);
+
+ assert.deepEqual(normalized.skills, {
+ summary: {
+ total_skill_loads: 0,
+ total_skill_edits: 0,
+ total_skill_actions: 0,
+ distinct_skills_used: 0,
+ },
+ top_skills: [],
+ });
+});
+
+test("normalizeAnalyticsResponse preserves populated skill analytics", () => {
+ const raw: AnalyticsResponse = {
+ daily: [],
+ by_model: [],
+ totals: {
+ total_input: 0,
+ total_output: 0,
+ total_cache_read: 0,
+ total_reasoning: 0,
+ total_estimated_cost: 0,
+ total_actual_cost: 0,
+ total_sessions: 0,
+ total_api_calls: 0,
+ },
+ skills: {
+ summary: {
+ total_skill_loads: 2,
+ total_skill_edits: 1,
+ total_skill_actions: 3,
+ distinct_skills_used: 2,
+ },
+ top_skills: [
+ {
+ skill: "systematic-debugging",
+ view_count: 2,
+ manage_count: 1,
+ total_count: 3,
+ percentage: 100,
+ last_used_at: 1713900000,
+ },
+ ],
+ },
+ };
+
+ const normalized = normalizeAnalyticsResponse(raw);
+
+ assert.deepEqual(normalized.skills, raw.skills);
+});
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index b4790f267..5c220c039 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -61,8 +61,8 @@ export const api = {
if (params.component && params.component !== "all") qs.set("component", params.component);
return fetchJSON(`/api/logs?${qs.toString()}`);
},
- getAnalytics: (days: number) =>
- fetchJSON(`/api/analytics/usage?days=${days}`),
+ getAnalytics: (days: number): Promise =>
+ fetchJSON(`/api/analytics/usage?days=${days}`).then(normalizeAnalyticsResponse),
getConfig: () => fetchJSON>("/api/config"),
getDefaults: () => fetchJSON>("/api/config/defaults"),
getSchema: () => fetchJSON<{ fields: Record; category_order: string[] }>("/api/config/schema"),
@@ -364,10 +364,32 @@ export interface AnalyticsResponse {
total_sessions: number;
total_api_calls: number;
};
+ skills?: {
+ summary?: AnalyticsSkillsSummary;
+ top_skills?: AnalyticsSkillEntry[];
+ };
+}
+
+export type NormalizedAnalyticsResponse = Omit & {
skills: {
summary: AnalyticsSkillsSummary;
top_skills: AnalyticsSkillEntry[];
};
+};
+
+export function normalizeAnalyticsResponse(raw: AnalyticsResponse): NormalizedAnalyticsResponse {
+ return {
+ ...raw,
+ skills: {
+ summary: {
+ total_skill_loads: raw.skills?.summary?.total_skill_loads ?? 0,
+ total_skill_edits: raw.skills?.summary?.total_skill_edits ?? 0,
+ total_skill_actions: raw.skills?.summary?.total_skill_actions ?? 0,
+ distinct_skills_used: raw.skills?.summary?.distinct_skills_used ?? 0,
+ },
+ top_skills: Array.isArray(raw.skills?.top_skills) ? raw.skills.top_skills : [],
+ },
+ };
}
export interface CronJob {
diff --git a/web/src/pages/AnalyticsPage.tsx b/web/src/pages/AnalyticsPage.tsx
index ba3061217..02b14ce2c 100644
--- a/web/src/pages/AnalyticsPage.tsx
+++ b/web/src/pages/AnalyticsPage.tsx
@@ -8,7 +8,12 @@ import {
TrendingUp,
} from "lucide-react";
import { api } from "@/lib/api";
-import type { AnalyticsResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsSkillEntry } from "@/lib/api";
+import type {
+ AnalyticsDailyEntry,
+ AnalyticsModelEntry,
+ AnalyticsSkillEntry,
+ NormalizedAnalyticsResponse,
+} from "@/lib/api";
import { timeAgo } from "@/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -280,7 +285,7 @@ function SkillTable({ skills }: { skills: AnalyticsSkillEntry[] }) {
export default function AnalyticsPage() {
const [days, setDays] = useState(30);
- const [data, setData] = useState(null);
+ const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { t } = useI18n();