Feature: Add API Usage Dashboard Showing Rate Limits and Quota Consumption
Description
AegisAI has per-user rate limiting on its API endpoints (Guard scan, RAG query, etc.) and stores rate limit data in Redis. However, there is no user-facing UI showing current usage, remaining quota, or rate limit history. Users hit 429 Too Many Requests errors with no way to monitor or understand their consumption patterns. This issue proposes adding an API Usage page that visualises request counts, rate limit hits, and remaining quota across all rate-limited endpoints, plus a per-endpoint breakdown.
Proposed Solution
Add a new GET /api/v1/analytics/usage endpoint that queries the Redis rate limiter for current counters (or a PostgreSQL api_usage_log table if persistent tracking is preferred). The frontend displays a dashboard with: a summary card showing remaining requests / total limit, a per-endpoint breakdown table with request count and limit, a time-series chart of requests over the last 7/30 days, and a list of recent 429 events. The Guard scan and RAG query endpoints get specific focus since they consume AI credits.
Acceptance Criteria
- A new "API Usage" page is accessible from the sidebar navigation (between Analytics and Settings)
- The page shows a summary card: "1,234 / 5,000 requests used today" with a progress bar colour-coded (green < 70%, yellow < 90%, red > 90%)
- A per-endpoint breakdown table with columns: Endpoint, Requests Today, Limit, Remaining, Reset Time
- A line chart (Recharts) showing daily request volume over the last 7 days (toggleable to 30 days)
- A "Recent Rate Limit Events" section listing the last 10 times the user hit a 429, with timestamp and endpoint
- The Guard scan endpoint is highlighted separately with its own card showing AI credit consumption
- The RAG query endpoint shows a separate card with query count and token usage estimate
- The data refreshes automatically every 60 seconds (or on page focus)
- An empty state is shown if no data is available yet
- Loading skeleton while data fetches
- The page respects mobile layout (stacks vertically)
Requirements
- Backend:
- New endpoint:
GET /api/v1/analytics/usage returns aggregated usage stats for the current user
- Reads rate limit counters from Redis (or a new
ApiUsageLog model for persistent storage)
- Returns:
{ daily: { total_requests, limit, remaining, reset_at }, endpoints: [...], history: [...], recent_429s: [...] }
- The endpoint itself should not be rate-limited or should count towards its own limit separately
- If Redis is not available, return mock/zero data gracefully
- Frontend:
ApiUsagePage component with cards, table, and chart
UsageSummaryCard — progress bar with count/limit
EndpointBreakdownTable — table of per-endpoint stats
UsageHistoryChart — Recharts line chart
RecentRateLimitEvents — list of 429 events
- Auto-refresh with
useInterval or TanStack Query's refetchInterval
- Theme-aware colours for charts (dark mode support)
Implementation Hints
# backend/app/api/v1/analytics.py
import json
from datetime import datetime, timedelta
@app.get("/api/v1/analytics/usage")
async def get_api_usage(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
redis_client: Redis = Depends(get_redis),
):
user_id = current_user.id
today = datetime.utcnow().date().isoformat()
usage_key = f"usage:{user_id}:{today}"
# Get daily counters from Redis
daily_data = {}
if redis_client:
daily_data = redis_client.hgetall(usage_key) or {}
# Per-endpoint breakdown
endpoints = [
{"name": "Guard Scan", "key": "guard_scan", "limit": 1000},
{"name": "RAG Query", "key": "rag_query", "limit": 500},
{"name": "AI System CRUD", "key": "ai_systems", "limit": 2000},
{"name": "Document Operations", "key": "documents", "limit": 1000},
{"name": "Classification", "key": "classification", "limit": 500},
]
endpoint_stats = []
total_requests = 0
total_limit = 0
for ep in endpoints:
count = int(daily_data.get(ep["key"], 0))
total_requests += count
total_limit += ep["limit"]
endpoint_stats.append({
"endpoint": ep["name"],
"requests": count,
"limit": ep["limit"],
"remaining": max(0, ep["limit"] - count),
"reset_at": (datetime.utcnow() + timedelta(days=1)).replace(hour=0, minute=0, second=0).isoformat(),
})
# History (last 7 days from persistent log or Redis)
history = []
if redis_client:
for i in range(7):
day = (datetime.utcnow() - timedelta(days=i)).date().isoformat()
day_key = f"usage:{user_id}:{day}"
day_total = sum(
int(v) for v in (redis_client.hgetall(day_key) or {}).values()
)
history.append({"date": day, "requests": day_total})
history.reverse()
# Recent 429 events
recent_429s = []
if redis_client:
limit_events = redis_client.lrange(f"rate_limit_events:{user_id}", 0, 9) or []
for event in limit_events:
try:
recent_429s.append(json.loads(event))
except json.JSONDecodeError:
continue
return {
"daily": {
"total_requests": total_requests,
"total_limit": total_limit,
"remaining": max(0, total_limit - total_requests),
"reset_at": (datetime.utcnow() + timedelta(days=1)).replace(hour=0, minute=0, second=0).isoformat(),
},
"endpoints": endpoint_stats,
"history": history,
"recent_429s": recent_429s,
"guard_scan": {
"requests": int(daily_data.get("guard_scan", 0)),
"limit": 1000,
"ai_credits_used": int(daily_data.get("guard_scan", 0)) * 0.5, # estimated
},
"rag_query": {
"requests": int(daily_data.get("rag_query", 0)),
"limit": 500,
"estimated_tokens": int(daily_data.get("rag_query", 0)) * 1500,
},
}
// frontend/src/pages/ApiUsagePage.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { Activity, AlertTriangle, Shield, BookOpen } from 'lucide-react';
import { api } from '@/services/api';
export const ApiUsagePage = () => {
const { data, isLoading } = useQuery({
queryKey: ['api-usage'],
queryFn: () => api.get('/api/v1/analytics/usage').then(r => r.data),
refetchInterval: 60_000, // auto-refresh every 60s
});
if (isLoading) return <ApiUsageSkeleton />;
const usagePercent = data ? (data.daily.total_requests / data.daily.total_limit) * 100 : 0;
return (
<div className="max-w-5xl mx-auto px-4 py-6 space-y-6">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Activity className="w-6 h-6 text-indigo-600" />
API Usage
</h1>
{/* Summary Card */}
<div className="bg-white dark:bg-slate-800 rounded-xl border p-6">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm text-gray-500">Daily Usage</p>
<p className="text-2xl font-bold">
{data.daily.total_requests.toLocaleString()} / {data.daily.total_limit.toLocaleString()}
</p>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
usagePercent > 90 ? 'bg-red-100 text-red-700' :
usagePercent > 70 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
{usagePercent.toFixed(0)}% used
</span>
</div>
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
usagePercent > 90 ? 'bg-red-500' : usagePercent > 70 ? 'bg-yellow-500' : 'bg-indigo-500'
}`}
style={{ width: `${Math.min(usagePercent, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-2">
Resets at {new Date(data.daily.reset_at).toLocaleString()}
</p>
</div>
{/* Guard Scan & RAG Highlight Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
<div className="flex items-center gap-2 mb-2">
<Shield className="w-5 h-5 text-purple-600" />
<h3 className="font-semibold">Guard Scan</h3>
</div>
<p className="text-2xl font-bold">{data.guard_scan.requests} / {data.guard_scan.limit}</p>
<p className="text-xs text-gray-500 mt-1">~{data.guard_scan.ai_credits_used.toFixed(1)} AI credits estimated</p>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
<div className="flex items-center gap-2 mb-2">
<BookOpen className="w-5 h-5 text-blue-600" />
<h3 className="font-semibold">RAG Query</h3>
</div>
<p className="text-2xl font-bold">{data.rag_query.requests} / {data.rag_query.limit}</p>
<p className="text-xs text-gray-500 mt-1">~{data.rag_query.estimated_tokens.toLocaleString()} tokens estimated</p>
</div>
</div>
{/* Per-Endpoint Breakdown */}
<div className="bg-white dark:bg-slate-800 rounded-xl border overflow-hidden">
<div className="p-4 border-b">
<h3 className="font-semibold">Endpoint Breakdown</h3>
</div>
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-slate-900">
<tr>
<th className="text-left p-3 font-medium">Endpoint</th>
<th className="text-right p-3 font-medium">Requests</th>
<th className="text-right p-3 font-medium">Limit</th>
<th className="text-right p-3 font-medium">Remaining</th>
<th className="text-right p-3 font-medium">Reset</th>
</tr>
</thead>
<tbody>
{data.endpoints.map(ep => (
<tr key={ep.endpoint} className="border-t">
<td className="p-3">{ep.endpoint}</td>
<td className="text-right p-3">{ep.requests.toLocaleString()}</td>
<td className="text-right p-3">{ep.limit.toLocaleString()}</td>
<td className={`text-right p-3 font-medium ${
ep.remaining < 10 ? 'text-red-600' : ep.remaining < 100 ? 'text-yellow-600' : ''
}`}>{ep.remaining.toLocaleString()}</td>
<td className="text-right p-3 text-gray-400 text-xs">{new Date(ep.reset_at).toLocaleTimeString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 7-Day History Chart */}
<div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
<h3 className="font-semibold mb-4">7-Day Request History</h3>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={data.history}>
<XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={d => new Date(d).toLocaleDateString('en', { month: 'short', day: 'numeric' })} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Line type="monotone" dataKey="requests" stroke="#6366f1" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
{/* Recent 429 Events */}
{data.recent_429s?.length > 0 && (
<div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-red-500" />
Recent Rate Limit Events
</h3>
<div className="space-y-2">
{data.recent_429s.map((event, i) => (
<div key={i} className="flex items-center justify-between text-sm p-2 bg-red-50 dark:bg-red-900/20 rounded-lg">
<span className="font-medium">{event.endpoint}</span>
<span className="text-gray-500 text-xs">{new Date(event.timestamp).toLocaleString()}</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const ApiUsageSkeleton = () => (
<div className="max-w-5xl mx-auto px-4 py-6 space-y-6 animate-pulse">
<div className="h-8 w-48 bg-gray-200 rounded" />
<div className="h-32 bg-gray-100 rounded-xl" />
<div className="grid grid-cols-2 gap-4">
<div className="h-24 bg-gray-100 rounded-xl" />
<div className="h-24 bg-gray-100 rounded-xl" />
</div>
<div className="h-48 bg-gray-100 rounded-xl" />
<div className="h-52 bg-gray-100 rounded-xl" />
</div>
);
Affected Files
backend/app/api/v1/analytics.py — add /usage endpoint
backend/app/core/deps.py — add Redis dependency injection (if not already)
backend/app/modules/guard/guard.py — log rate limit events to Redis
frontend/src/pages/ApiUsagePage.tsx — new API usage page
frontend/src/App.tsx — add route for /usage or /api-usage
frontend/src/components/layout/Sidebar.tsx — add "API Usage" nav link
Labels
type:feature, level:beginner, GSSoC-26
Feature: Add API Usage Dashboard Showing Rate Limits and Quota Consumption
Description
AegisAI has per-user rate limiting on its API endpoints (Guard scan, RAG query, etc.) and stores rate limit data in Redis. However, there is no user-facing UI showing current usage, remaining quota, or rate limit history. Users hit 429 Too Many Requests errors with no way to monitor or understand their consumption patterns. This issue proposes adding an API Usage page that visualises request counts, rate limit hits, and remaining quota across all rate-limited endpoints, plus a per-endpoint breakdown.
Proposed Solution
Add a new
GET /api/v1/analytics/usageendpoint that queries the Redis rate limiter for current counters (or a PostgreSQLapi_usage_logtable if persistent tracking is preferred). The frontend displays a dashboard with: a summary card showing remaining requests / total limit, a per-endpoint breakdown table with request count and limit, a time-series chart of requests over the last 7/30 days, and a list of recent 429 events. The Guard scan and RAG query endpoints get specific focus since they consume AI credits.Acceptance Criteria
Requirements
GET /api/v1/analytics/usagereturns aggregated usage stats for the current userApiUsageLogmodel for persistent storage){ daily: { total_requests, limit, remaining, reset_at }, endpoints: [...], history: [...], recent_429s: [...] }ApiUsagePagecomponent with cards, table, and chartUsageSummaryCard— progress bar with count/limitEndpointBreakdownTable— table of per-endpoint statsUsageHistoryChart— Recharts line chartRecentRateLimitEvents— list of 429 eventsuseIntervalor TanStack Query'srefetchIntervalImplementation Hints
Affected Files
backend/app/api/v1/analytics.py— add/usageendpointbackend/app/core/deps.py— add Redis dependency injection (if not already)backend/app/modules/guard/guard.py— log rate limit events to Redisfrontend/src/pages/ApiUsagePage.tsx— new API usage pagefrontend/src/App.tsx— add route for/usageor/api-usagefrontend/src/components/layout/Sidebar.tsx— add "API Usage" nav linkLabels
type:feature,level:beginner,GSSoC-26