@nordsym/apiclaw 1.5.5 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DASHBOARD_FIX.md +29 -0
- package/convex/analytics.ts +96 -0
- package/convex/logs.ts +22 -2
- package/convex/schema.ts +2 -0
- package/convex/workspaces.ts +10 -1
- package/landing/src/app/api/og/route.tsx +0 -2
- package/landing/src/app/page.tsx +1 -3
- package/landing/src/app/workspace/page.tsx +32 -10
- package/landing/src/lib/stats.json +2 -3
- package/package.json +1 -1
package/DASHBOARD_FIX.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# APIClaw Dashboard Fix Plan
|
|
2
|
+
|
|
3
|
+
## Issues Found:
|
|
4
|
+
|
|
5
|
+
### 1. Agent Count Mismatch (8 vs 1)
|
|
6
|
+
**Root cause:** Overview counts `agentSessions` (stale), My Agents tab queries `agents` table (correct)
|
|
7
|
+
**Fix:** Update `getWorkspaceDashboard` to count from `agents` table instead
|
|
8
|
+
|
|
9
|
+
### 2. Analytics = Preview Mode
|
|
10
|
+
**Root cause:** `analytics:getProviderBreakdown` doesn't exist
|
|
11
|
+
**Fix:** Create query that aggregates from `apiLogs` table
|
|
12
|
+
|
|
13
|
+
### 3. Usage Count (claims 7, actually 1)
|
|
14
|
+
**Diagnosis:** Backend correctly shows 1, dashboard may be cached
|
|
15
|
+
**Fix:** Hard refresh should resolve, but verify incrementUsage is called
|
|
16
|
+
|
|
17
|
+
### 4. lastActiveAt Frozen
|
|
18
|
+
**Diagnosis:** Not being updated on proxy calls
|
|
19
|
+
**Fix:** Update agent.lastActiveAt when logging proxy call
|
|
20
|
+
|
|
21
|
+
## Implementation Order:
|
|
22
|
+
|
|
23
|
+
1. Create `analytics:getProviderBreakdown` query
|
|
24
|
+
2. Fix `getWorkspaceDashboard` to count agents correctly
|
|
25
|
+
3. Update `createProxyLog` to touch agent lastActiveAt
|
|
26
|
+
4. Remove preview fallback in frontend
|
|
27
|
+
5. Rename "Direct Call" → "API Catalog"
|
|
28
|
+
|
|
29
|
+
Time estimate: 30 mins
|
package/convex/analytics.ts
CHANGED
|
@@ -88,3 +88,99 @@ export const getRecent = query({
|
|
|
88
88
|
.take(limit);
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
// Get provider breakdown for Agent Analytics (workspace-specific)
|
|
93
|
+
export const getProviderBreakdown = query({
|
|
94
|
+
args: {
|
|
95
|
+
token: v.string(),
|
|
96
|
+
periodDays: v.optional(v.number()),
|
|
97
|
+
},
|
|
98
|
+
handler: async (ctx, args) => {
|
|
99
|
+
const periodDays = args.periodDays || 7;
|
|
100
|
+
const since = Date.now() - periodDays * 24 * 3600000;
|
|
101
|
+
|
|
102
|
+
// Verify session and get workspace
|
|
103
|
+
const session = await ctx.db
|
|
104
|
+
.query("agentSessions")
|
|
105
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
106
|
+
.first();
|
|
107
|
+
|
|
108
|
+
if (!session) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get all API logs for this workspace
|
|
113
|
+
const logs = await ctx.db
|
|
114
|
+
.query("apiLogs")
|
|
115
|
+
.withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
|
|
116
|
+
.filter((q) => q.gte(q.field("createdAt"), since))
|
|
117
|
+
.collect();
|
|
118
|
+
|
|
119
|
+
if (logs.length === 0) {
|
|
120
|
+
return null; // Return null so frontend knows to show empty state, not preview
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Aggregate stats
|
|
124
|
+
const totalCalls = logs.length;
|
|
125
|
+
const successCount = logs.filter((l) => l.status === "success").length;
|
|
126
|
+
const failureCount = logs.filter((l) => l.status === "error").length;
|
|
127
|
+
const totalLatency = logs.reduce((sum, l) => sum + (l.latencyMs || 0), 0);
|
|
128
|
+
const avgLatency = totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0;
|
|
129
|
+
|
|
130
|
+
// Provider breakdown
|
|
131
|
+
const byProvider: Record<string, { count: number; latency: number }> = {};
|
|
132
|
+
for (const log of logs) {
|
|
133
|
+
if (!byProvider[log.provider]) {
|
|
134
|
+
byProvider[log.provider] = { count: 0, latency: 0 };
|
|
135
|
+
}
|
|
136
|
+
byProvider[log.provider].count++;
|
|
137
|
+
byProvider[log.provider].latency += log.latencyMs || 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Agent breakdown (by subagentId)
|
|
141
|
+
const byAgent: Record<string, number> = {};
|
|
142
|
+
for (const log of logs) {
|
|
143
|
+
const agent = log.subagentId || "main";
|
|
144
|
+
byAgent[agent] = (byAgent[agent] || 0) + 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Action breakdown
|
|
148
|
+
const byAction: Record<string, number> = {};
|
|
149
|
+
for (const log of logs) {
|
|
150
|
+
const key = `${log.provider}:${log.action}`;
|
|
151
|
+
byAction[key] = (byAction[key] || 0) + 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Time series (daily)
|
|
155
|
+
const dailyCounts: Record<string, number> = {};
|
|
156
|
+
for (const log of logs) {
|
|
157
|
+
const day = new Date(log.createdAt).toISOString().slice(0, 10);
|
|
158
|
+
dailyCounts[day] = (dailyCounts[day] || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
totalCalls,
|
|
163
|
+
successCount,
|
|
164
|
+
failureCount,
|
|
165
|
+
successRate: totalCalls > 0 ? (successCount / totalCalls) * 100 : 0,
|
|
166
|
+
avgLatency,
|
|
167
|
+
byProvider: Object.entries(byProvider).map(([name, data]) => ({
|
|
168
|
+
name,
|
|
169
|
+
count: data.count,
|
|
170
|
+
avgLatency: data.count > 0 ? Math.round(data.latency / data.count) : 0,
|
|
171
|
+
})).sort((a, b) => b.count - a.count),
|
|
172
|
+
byAgent: Object.entries(byAgent).map(([name, count]) => ({
|
|
173
|
+
name,
|
|
174
|
+
count,
|
|
175
|
+
})).sort((a, b) => b.count - a.count),
|
|
176
|
+
byAction: Object.entries(byAction).map(([name, count]) => ({
|
|
177
|
+
name,
|
|
178
|
+
count,
|
|
179
|
+
})).sort((a, b) => b.count - a.count).slice(0, 10),
|
|
180
|
+
timeSeries: Object.entries(dailyCounts)
|
|
181
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
182
|
+
.map(([date, count]) => ({ date, count })),
|
|
183
|
+
isPreview: false,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
package/convex/logs.ts
CHANGED
|
@@ -364,8 +364,9 @@ export const getLogStats = query({
|
|
|
364
364
|
}))
|
|
365
365
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
366
366
|
|
|
367
|
-
// Get unique providers for filter
|
|
367
|
+
// Get unique providers and agents for filter dropdowns
|
|
368
368
|
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
369
|
+
const agents = [...new Set(logs.map((l) => l.subagentId || "main"))].sort();
|
|
369
370
|
|
|
370
371
|
return {
|
|
371
372
|
totalCalls,
|
|
@@ -376,6 +377,7 @@ export const getLogStats = query({
|
|
|
376
377
|
byProvider,
|
|
377
378
|
byDay,
|
|
378
379
|
providers,
|
|
380
|
+
agents,
|
|
379
381
|
};
|
|
380
382
|
},
|
|
381
383
|
});
|
|
@@ -514,6 +516,8 @@ export const createProxyLog = mutation({
|
|
|
514
516
|
sessionToken: v.optional(v.string()),
|
|
515
517
|
},
|
|
516
518
|
handler: async (ctx, { workspaceId, provider, action, subagentId, sessionToken }) => {
|
|
519
|
+
const now = Date.now();
|
|
520
|
+
|
|
517
521
|
await ctx.db.insert("apiLogs", {
|
|
518
522
|
workspaceId,
|
|
519
523
|
provider,
|
|
@@ -522,9 +526,25 @@ export const createProxyLog = mutation({
|
|
|
522
526
|
sessionToken: sessionToken || "proxy",
|
|
523
527
|
status: "success",
|
|
524
528
|
latencyMs: 0, // Proxy calls don't track latency
|
|
525
|
-
createdAt:
|
|
529
|
+
createdAt: now,
|
|
526
530
|
});
|
|
527
531
|
|
|
532
|
+
// Update workspace lastActiveAt (main agent activity)
|
|
533
|
+
await ctx.db.patch(workspaceId, { lastActiveAt: now });
|
|
534
|
+
|
|
535
|
+
// If this is a subagent call, update that subagent's timestamp
|
|
536
|
+
if (subagentId && subagentId !== "unknown" && subagentId !== "main") {
|
|
537
|
+
const subagent = await ctx.db
|
|
538
|
+
.query("subagents")
|
|
539
|
+
.withIndex("by_workspaceId_subagentId", (q) =>
|
|
540
|
+
q.eq("workspaceId", workspaceId).eq("subagentId", subagentId))
|
|
541
|
+
.first();
|
|
542
|
+
|
|
543
|
+
if (subagent) {
|
|
544
|
+
await ctx.db.patch(subagent._id, { lastActiveAt: now });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
528
548
|
return { success: true };
|
|
529
549
|
},
|
|
530
550
|
});
|
package/convex/schema.ts
CHANGED
|
@@ -84,6 +84,8 @@ export default defineSchema({
|
|
|
84
84
|
pauseOnBudgetExceeded: v.optional(v.boolean()), // If true, block execution when budget exceeded
|
|
85
85
|
monthlySpendCents: v.optional(v.number()), // Current month's spend in cents
|
|
86
86
|
lastSpendResetAt: v.optional(v.number()), // When monthly spend was last reset
|
|
87
|
+
// Activity tracking
|
|
88
|
+
lastActiveAt: v.optional(v.number()), // Last API call timestamp (main agent)
|
|
87
89
|
createdAt: v.number(),
|
|
88
90
|
updatedAt: v.number(),
|
|
89
91
|
})
|
package/convex/workspaces.ts
CHANGED
|
@@ -226,6 +226,15 @@ export const getWorkspaceDashboard = query({
|
|
|
226
226
|
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
227
227
|
.collect();
|
|
228
228
|
|
|
229
|
+
// Count agents: 1 main agent (if exists) + subagents
|
|
230
|
+
const hasMainAgent = workspace.mainAgentId ? 1 : 0;
|
|
231
|
+
const subagents = await ctx.db
|
|
232
|
+
.query("subagents")
|
|
233
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
234
|
+
.collect();
|
|
235
|
+
|
|
236
|
+
const totalAgentCount = hasMainAgent + subagents.length;
|
|
237
|
+
|
|
229
238
|
// Get usage logs for this workspace (via agent credits or purchases)
|
|
230
239
|
const credits = await ctx.db
|
|
231
240
|
.query("agentCredits")
|
|
@@ -271,7 +280,7 @@ export const getWorkspaceDashboard = query({
|
|
|
271
280
|
createdAt: workspace.createdAt,
|
|
272
281
|
},
|
|
273
282
|
stats: {
|
|
274
|
-
totalAgents:
|
|
283
|
+
totalAgents: totalAgentCount,
|
|
275
284
|
totalCredits: workspaceCredits.reduce((sum, c) => sum + c.balanceUsd, 0),
|
|
276
285
|
totalPurchases: workspacePurchases.length,
|
|
277
286
|
},
|
package/landing/src/app/page.tsx
CHANGED
|
@@ -14,10 +14,8 @@ import { PhoneDemo } from "@/components/demo";
|
|
|
14
14
|
import { AITestimonials } from "@/components/AITestimonials";
|
|
15
15
|
|
|
16
16
|
const stats = [
|
|
17
|
-
{ number: "4,232+", label: "Installs", live: true },
|
|
18
17
|
{ number: statsData.apiCount.toLocaleString(), label: "APIs Indexed", live: true },
|
|
19
18
|
{ number: statsData.openApiCount.toLocaleString(), label: "Open APIs", live: true },
|
|
20
|
-
{ number: statsData.directCallCount.toString(), label: "Direct Call", live: true },
|
|
21
19
|
{ number: statsData.categoryCount.toString(), label: "Categories", live: false },
|
|
22
20
|
];
|
|
23
21
|
|
|
@@ -211,7 +209,7 @@ Instant highlights:
|
|
|
211
209
|
• Code sandbox (E2B)
|
|
212
210
|
• Multi-LLM routing (OpenRouter)
|
|
213
211
|
|
|
214
|
-
|
|
212
|
+
Direct Call providers across categories:
|
|
215
213
|
AI/ML: Replicate, OpenRouter, Groq, Mistral, Cohere, Together AI, Stability AI
|
|
216
214
|
Voice: ElevenLabs, Deepgram, AssemblyAI
|
|
217
215
|
Search: Brave Search, Serper, Firecrawl
|
|
@@ -401,12 +401,12 @@ export default function WorkspacePage() {
|
|
|
401
401
|
if (analytics && typeof analytics === "object" && !analytics.status) {
|
|
402
402
|
setProviderAnalytics(analytics);
|
|
403
403
|
} else {
|
|
404
|
-
//
|
|
405
|
-
setProviderAnalytics(
|
|
404
|
+
// No analytics available - will show empty state
|
|
405
|
+
setProviderAnalytics(null);
|
|
406
406
|
}
|
|
407
407
|
} catch {
|
|
408
|
-
//
|
|
409
|
-
setProviderAnalytics(
|
|
408
|
+
// Error fetching analytics - will show empty state
|
|
409
|
+
setProviderAnalytics(null);
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
} catch (err) {
|
|
@@ -430,9 +430,6 @@ export default function WorkspacePage() {
|
|
|
430
430
|
// Check provider session and fetch APIs
|
|
431
431
|
await fetchProviderData();
|
|
432
432
|
|
|
433
|
-
// Always ensure preview analytics exist for Analytics tab
|
|
434
|
-
setProviderAnalytics(prev => prev || generatePreviewAnalytics());
|
|
435
|
-
|
|
436
433
|
// If neither session type, redirect to login
|
|
437
434
|
if (!token && !localStorage.getItem("apiclaw_session")) {
|
|
438
435
|
router.push("/login");
|
|
@@ -524,7 +521,7 @@ export default function WorkspacePage() {
|
|
|
524
521
|
// Main navigation tabs
|
|
525
522
|
const mainTabs = [
|
|
526
523
|
{ id: "overview" as TabType, label: "Overview", icon: Home },
|
|
527
|
-
{ id: "api-catalog" as TabType, label: "
|
|
524
|
+
{ id: "api-catalog" as TabType, label: "API Catalog", icon: Zap },
|
|
528
525
|
{ id: "my-agents" as TabType, label: "My Agents", icon: Users },
|
|
529
526
|
{ id: "my-apis" as TabType, label: "My APIs", icon: Terminal },
|
|
530
527
|
{ id: "analytics" as TabType, label: "Analytics", icon: BarChart3, hasDropdown: true },
|
|
@@ -3460,7 +3457,7 @@ const typeBadges: Record<string, { icon: typeof Search; label: string; className
|
|
|
3460
3457
|
},
|
|
3461
3458
|
direct_call: {
|
|
3462
3459
|
icon: Zap,
|
|
3463
|
-
label: "
|
|
3460
|
+
label: "API Catalog",
|
|
3464
3461
|
className: "bg-green-500/10 text-green-500 border border-green-500/20"
|
|
3465
3462
|
},
|
|
3466
3463
|
chain: {
|
|
@@ -3489,6 +3486,7 @@ interface ApiLogEntry {
|
|
|
3489
3486
|
status: "success" | "error";
|
|
3490
3487
|
latencyMs: number;
|
|
3491
3488
|
errorMessage?: string;
|
|
3489
|
+
subagentId?: string;
|
|
3492
3490
|
createdAt: number;
|
|
3493
3491
|
}
|
|
3494
3492
|
|
|
@@ -3519,7 +3517,9 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3519
3517
|
const [isLoading, setIsLoading] = useState(true);
|
|
3520
3518
|
const [statusFilter, setStatusFilter] = useState<"all" | "success" | "error">("all");
|
|
3521
3519
|
const [providerFilter, setProviderFilter] = useState<string>("all");
|
|
3520
|
+
const [agentFilter, setAgentFilter] = useState<string>("all");
|
|
3522
3521
|
const [providers, setProviders] = useState<string[]>([]);
|
|
3522
|
+
const [agents, setAgents] = useState<string[]>([]);
|
|
3523
3523
|
const [hasMore, setHasMore] = useState(false);
|
|
3524
3524
|
const [nextCursor, setNextCursor] = useState<number | undefined>();
|
|
3525
3525
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
@@ -3548,6 +3548,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3548
3548
|
cursor,
|
|
3549
3549
|
status: statusFilter,
|
|
3550
3550
|
provider: providerFilter === "all" ? undefined : providerFilter,
|
|
3551
|
+
subagentId: agentFilter === "all" ? undefined : agentFilter,
|
|
3551
3552
|
},
|
|
3552
3553
|
}),
|
|
3553
3554
|
});
|
|
@@ -3605,7 +3606,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3605
3606
|
setIsLoading(false);
|
|
3606
3607
|
setLoadingMore(false);
|
|
3607
3608
|
}
|
|
3608
|
-
}, [sessionToken, statusFilter, providerFilter, nextCursor]);
|
|
3609
|
+
}, [sessionToken, statusFilter, providerFilter, agentFilter, nextCursor]);
|
|
3609
3610
|
|
|
3610
3611
|
const fetchStats = useCallback(async () => {
|
|
3611
3612
|
if (!sessionToken) return;
|
|
@@ -3624,6 +3625,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3624
3625
|
const result = statsData.value || statsData;
|
|
3625
3626
|
setStats(result);
|
|
3626
3627
|
setProviders(result.providers || []);
|
|
3628
|
+
setAgents(result.agents || []);
|
|
3627
3629
|
} catch (err) {
|
|
3628
3630
|
console.error("Error fetching stats:", err);
|
|
3629
3631
|
}
|
|
@@ -3695,6 +3697,18 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3695
3697
|
<option key={p} value={p}>{p}</option>
|
|
3696
3698
|
))}
|
|
3697
3699
|
</select>
|
|
3700
|
+
|
|
3701
|
+
<select
|
|
3702
|
+
value={agentFilter}
|
|
3703
|
+
onChange={(e) => setAgentFilter(e.target.value)}
|
|
3704
|
+
className="px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
|
|
3705
|
+
>
|
|
3706
|
+
<option value="all">All Agents</option>
|
|
3707
|
+
<option value="main">Main Agent</option>
|
|
3708
|
+
{agents.filter(a => a !== "main" && a !== "unknown").map((a) => (
|
|
3709
|
+
<option key={a} value={a}>{a}</option>
|
|
3710
|
+
))}
|
|
3711
|
+
</select>
|
|
3698
3712
|
</div>
|
|
3699
3713
|
|
|
3700
3714
|
{/* Stats Cards */}
|
|
@@ -3756,6 +3770,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3756
3770
|
<thead className="bg-[var(--surface)]">
|
|
3757
3771
|
<tr>
|
|
3758
3772
|
<th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Type</th>
|
|
3773
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Agent</th>
|
|
3759
3774
|
<th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Time</th>
|
|
3760
3775
|
<th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Provider</th>
|
|
3761
3776
|
<th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Action</th>
|
|
@@ -3769,6 +3784,13 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
|
|
|
3769
3784
|
<td className="px-4 py-3">
|
|
3770
3785
|
<TypeBadge type={log.type} />
|
|
3771
3786
|
</td>
|
|
3787
|
+
<td className="px-4 py-3">
|
|
3788
|
+
<span className="text-sm font-mono text-[var(--text-muted)]">
|
|
3789
|
+
{log.type === "direct_call"
|
|
3790
|
+
? ((log as ApiLogEntry).subagentId || "main")
|
|
3791
|
+
: "—"}
|
|
3792
|
+
</span>
|
|
3793
|
+
</td>
|
|
3772
3794
|
<td className="px-4 py-3 text-sm text-[var(--text-muted)]">
|
|
3773
3795
|
{formatTime(log.createdAt)}
|
|
3774
3796
|
</td>
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
"openApiCount": 1636,
|
|
4
4
|
"directCallCount": 18,
|
|
5
5
|
"categoryCount": 13,
|
|
6
|
-
"npmDownloads": 4232,
|
|
7
6
|
"lastUpdated": "2026-02-27T09:10:41.344767",
|
|
8
|
-
"generatedAt": "2026-03-
|
|
7
|
+
"generatedAt": "2026-03-18T11:41:41.087Z",
|
|
9
8
|
"categoryBreakdown": {
|
|
10
9
|
"Finance": 1179,
|
|
11
10
|
"Auth & Security": 491,
|
|
@@ -21,4 +20,4 @@
|
|
|
21
20
|
"Entertainment": 1212,
|
|
22
21
|
"Health & Fitness": 740
|
|
23
22
|
}
|
|
24
|
-
}
|
|
23
|
+
}
|