Replace mock dashboard data with real backend data, remove unbuilt pages
Dashboard home was hardcoding fake numbers (2,431/10,000 requests, fake "Pro Plan", fake activity log, fake random bar chart) despite the real analytics/logs/keys endpoints already existing and being used correctly elsewhere (Usage, Logs, Tokens pages). A fresh account with zero real activity was showing 24% "used" from nobody's data. Rewired every stat card, the usage chart, and the activity table to real /api/analytics/*, /api/logs, and /api/keys data. Every Quick Action now does something real (navigate to the real page, or call a real endpoint) instead of being a no-op or hardcoded button. Also removed 5 sidebar/footer links (AI Setup Guides, Tools & Permissions, Billing, Team, Settings, Support) that pointed at nonexistent routes or pages with no backend feature behind them at all — no team/multi-user system, no per-tool permission toggles, no real subscription/billing system exist in the backend yet. Documented what's needed to build each one for real in FUTURE_FEATURES.md rather than shipping more fake UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0444ef09aa
commit
739740bb4b
83
FUTURE_FEATURES.md
Normal file
83
FUTURE_FEATURES.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Removed / Not-Yet-Built Features
|
||||
|
||||
These sidebar pages were removed from `src/pages/dashboard/DashboardLayout.tsx`'s
|
||||
nav because no backend feature exists for them — they were showing fully
|
||||
hardcoded mock data with no real API behind them. Removed rather than shipped
|
||||
with fake data, per the "no mock data" rule.
|
||||
|
||||
## 1. Billing / Subscriptions
|
||||
|
||||
**Was at:** `/dashboard/billing` (deleted: `src/pages/dashboard/Billing.tsx`)
|
||||
|
||||
**What it showed (all fake):** hardcoded "Pro Plan $49/month", 3-tier plan
|
||||
comparison cards, fake invoice history table.
|
||||
|
||||
**What's needed to rebuild for real:**
|
||||
- Backend: `plan`, `subscription_status`, `stripe_customer_id` columns on
|
||||
`users` table (mentioned as a possibility in `HANDOFF.md` from the original
|
||||
handoff, never implemented)
|
||||
- Real Stripe integration — the old Next.js frontend had
|
||||
`src/app/api/stripe/{checkout,webhook}/route.ts` but they were dead code
|
||||
(never called, placeholder keys, no backend wiring). Since Vite can't host
|
||||
secret-key server logic, this needs to live as new endpoints on the FastAPI
|
||||
backend (`Multi-Tenant-ODOO-MCP`), not the frontend.
|
||||
- New endpoints: `POST /api/billing/checkout`, `POST /api/billing/webhook`,
|
||||
`GET /api/billing/invoices`
|
||||
- Frontend: rebuild the page against those real endpoints
|
||||
|
||||
## 2. Team / Multi-User Accounts
|
||||
|
||||
**Was at:** `/dashboard/team` (route never existed — dead nav link, 404)
|
||||
|
||||
**What's needed:** an entirely new multi-user-per-account concept. Currently
|
||||
one `users` row = one account = one set of Odoo credentials/API keys. Team
|
||||
support means either:
|
||||
- A new `team_members` table linking multiple `users` rows to one billing
|
||||
account with role-based permissions, or
|
||||
- An `account_id` grouping concept above `users`
|
||||
|
||||
This is a significant schema/auth redesign, not a small addition.
|
||||
|
||||
## 3. Tools & Permissions
|
||||
|
||||
**Was at:** `/dashboard/tools` (route never existed — dead nav link, 404)
|
||||
|
||||
**What's needed:** per-user, per-tool enable/disable toggles for the 47 MCP
|
||||
tools currently registered in `server.py`'s `TOOLS` list. Would need:
|
||||
- A new `disabled_tools` table or JSON column on `users`
|
||||
- A check in `call_tool()` (`server.py`) before dispatch: reject if the tool
|
||||
is disabled for that user
|
||||
- Frontend: a page listing all 47 tools grouped by module (connection,
|
||||
discovery, records, workflow, smart) with toggles, calling a new
|
||||
`PATCH /api/tools/{name}` endpoint
|
||||
|
||||
## 4. AI Setup Guides
|
||||
|
||||
**Was at:** `/dashboard/guides` (route never existed — dead nav link, 404)
|
||||
|
||||
**What's needed:** this is content, not really a backend feature — step-by-step
|
||||
setup instructions for Claude, Codex, Cursor, Windsurf. Partially exists
|
||||
already: `/dashboard/endpoints` already has a working Claude Desktop JSON
|
||||
config generator with the real API key and MCP URL. This page could be built
|
||||
as a pure frontend content page (no backend needed) with setup docs for the
|
||||
other clients, following the same pattern already in `Endpoints.tsx`.
|
||||
|
||||
## 5. Settings (account settings / change password while logged in)
|
||||
|
||||
**Was at:** `/dashboard/settings` (route never existed — dead nav link, 404)
|
||||
|
||||
**What's needed:** currently the only way to change a password is the
|
||||
forgot-password token-based flow (`/forgot-password` → `/reset-password`).
|
||||
There's no "logged in, change my password" endpoint. Would need:
|
||||
- Backend: `POST /api/account/change-password` (current password + new
|
||||
password, no token needed since the user is already authenticated)
|
||||
- Frontend: a settings page with that form, plus maybe email/display-name
|
||||
fields if those become editable later
|
||||
|
||||
## 6. Support link
|
||||
|
||||
**Was at:** footer link `/dashboard/support` (route never existed — dead
|
||||
link, removed entirely from `DashboardLayout.tsx`, not just hidden)
|
||||
|
||||
Trivial — just needs a real destination (mailto link, external help site, or
|
||||
a real support-ticket feature) whenever there's something to point it at.
|
||||
@ -8,7 +8,6 @@ import ResetPassword from './pages/ResetPassword';
|
||||
import DashboardLayout from './pages/dashboard/DashboardLayout';
|
||||
import DashboardHome from './pages/dashboard/DashboardHome';
|
||||
import Connections from './pages/dashboard/Connections';
|
||||
import Billing from './pages/dashboard/Billing';
|
||||
import Endpoints from './pages/dashboard/Endpoints';
|
||||
import Tokens from './pages/dashboard/Tokens';
|
||||
import Usage from './pages/dashboard/Usage';
|
||||
@ -33,7 +32,6 @@ export default function App() {
|
||||
<Route path="/dashboard" element={<DashboardLayout />}>
|
||||
<Route index element={<DashboardHome />} />
|
||||
<Route path="connections" element={<Connections />} />
|
||||
<Route path="billing" element={<Billing />} />
|
||||
<Route path="endpoints" element={<Endpoints />} />
|
||||
<Route path="tokens" element={<Tokens />} />
|
||||
<Route path="usage" element={<Usage />} />
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
const PLANS = [
|
||||
{ name: 'Starter', price: 19, current: false, features: ['1 connection', '1 MCP endpoint', 'Read-only', '1,000 req/mo'] },
|
||||
{ name: 'Pro', price: 49, current: true, features: ['3 connections', 'Read/Write', 'Access logs', '10,000 req/mo'] },
|
||||
{ name: 'Agency', price: 149, current: false, features: ['25 workspaces', 'White-label', 'Team members', '50,000 req/mo'] },
|
||||
];
|
||||
|
||||
export default function Billing() {
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Billing</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">Manage your subscription and payment details.</p>
|
||||
</div>
|
||||
|
||||
{/* Current plan */}
|
||||
<div className="bg-white rounded-xl border border-brand-200 shadow-sm p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Current Plan</p>
|
||||
<p className="text-lg font-bold text-gray-900">Pro Plan</p>
|
||||
<p className="text-sm text-gray-500">$49/month · Renews July 29, 2026</p>
|
||||
</div>
|
||||
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1.5 rounded-full">Active</span>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-3">
|
||||
<button className="text-sm border border-gray-200 text-gray-600 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
Manage Payment Method
|
||||
</button>
|
||||
<button className="text-sm text-red-600 border border-red-200 px-4 py-2 rounded-lg hover:bg-red-50 transition-colors">
|
||||
Cancel Subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan comparison */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Change Plan</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{PLANS.map((plan) => (
|
||||
<div key={plan.name}
|
||||
className={`rounded-xl border p-5 ${plan.current ? 'border-brand-400 bg-brand-50' : 'border-gray-200 bg-white'}`}>
|
||||
<p className="font-semibold text-sm text-gray-900">{plan.name}</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1 mb-3">${plan.price}<span className="text-sm font-normal text-gray-400">/mo</span></p>
|
||||
<ul className="space-y-1.5 mb-4">
|
||||
{plan.features.map((f) => (
|
||||
<li key={f} className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<Check size={11} className="text-brand-500" /> {f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{plan.current ? (
|
||||
<div className="text-center text-xs text-brand-600 font-medium py-2">Current Plan</div>
|
||||
) : (
|
||||
<button className="w-full gradient-brand text-white py-2 rounded-lg text-xs font-medium hover:opacity-90 transition-opacity">
|
||||
Switch to {plan.name}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice history */}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Invoice History</h2>
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-100">
|
||||
<th className="text-left pb-2 font-medium">Date</th>
|
||||
<th className="text-left pb-2 font-medium">Description</th>
|
||||
<th className="text-left pb-2 font-medium">Amount</th>
|
||||
<th className="text-left pb-2 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{[
|
||||
{ date: 'Jun 29, 2026', desc: 'Pro Plan — Monthly', amount: '$49.00', status: 'Paid' },
|
||||
{ date: 'May 29, 2026', desc: 'Pro Plan — Monthly', amount: '$49.00', status: 'Paid' },
|
||||
{ date: 'Apr 29, 2026', desc: 'Pro Plan — Monthly', amount: '$49.00', status: 'Paid' },
|
||||
].map((row, i) => (
|
||||
<tr key={i}>
|
||||
<td className="py-2.5 text-gray-500">{row.date}</td>
|
||||
<td className="py-2.5 text-gray-700">{row.desc}</td>
|
||||
<td className="py-2.5 text-gray-900 font-medium">{row.amount}</td>
|
||||
<td className="py-2.5">
|
||||
<span className="bg-green-50 text-green-600 px-2 py-0.5 rounded-full">{row.status}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,30 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, session, type MeResult } from '@/lib/api';
|
||||
import { Cpu, Globe, BarChart2, Copy, RotateCcw, ExternalLink, Activity } from 'lucide-react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
api, session, type MeResult, type AnalyticsSummary, type TimeseriesBucket, type LogEntry,
|
||||
} from '@/lib/api';
|
||||
import {
|
||||
Cpu, Globe, Copy, RotateCcw, ExternalLink, Activity, CheckCircle2, XCircle, Key,
|
||||
} from 'lucide-react';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
const MCP_URL = `${import.meta.env.VITE_API_URL ?? 'https://odoo-mcp.thedomainnest.com'}/mcp/sse`;
|
||||
|
||||
const RECENT_ACTIVITY = [
|
||||
{ time: '2 mins ago', event: 'Connection sync', status: 'Success', detail: 'Odoo connection is healthy' },
|
||||
{ time: '5 mins ago', event: 'MCP request', status: 'Success', detail: 'search_invoices' },
|
||||
{ time: '12 mins ago', event: 'Token rotated', status: 'Success', detail: 'New token generated' },
|
||||
{ time: '1 hour ago', event: 'Connection sync', status: 'Success', detail: 'Odoo connection is healthy' },
|
||||
];
|
||||
function isoDaysAgo(days: number): string {
|
||||
const d = new Date();
|
||||
d.setUTCDate(d.getUTCDate() - days);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export default function DashboardHome() {
|
||||
const navigate = useNavigate();
|
||||
const [me, setMe] = useState<MeResult | null>(null);
|
||||
const [summary, setSummary] = useState<AnalyticsSummary | null>(null);
|
||||
const [timeseries, setTimeseries] = useState<TimeseriesBucket[]>([]);
|
||||
const [recentLogs, setRecentLogs] = useState<LogEntry[]>([]);
|
||||
const [activeKeyCount, setActiveKeyCount] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [rotating, setRotating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const load = useCallback(() => {
|
||||
const key = session.getKey();
|
||||
if (!key) return;
|
||||
api.me(key).then(setMe).catch(console.error).finally(() => setLoading(false));
|
||||
setLoading(true);
|
||||
const since = isoDaysAgo(30);
|
||||
Promise.all([
|
||||
api.me(key),
|
||||
api.analyticsSummary(key, since),
|
||||
api.analyticsTimeseries(key, since, undefined, 'day'),
|
||||
api.listLogs(key, 1, 5),
|
||||
api.listApiKeys(key),
|
||||
])
|
||||
.then(([meResult, summaryResult, timeseriesResult, logsResult, keysResult]) => {
|
||||
setMe(meResult);
|
||||
setSummary(summaryResult);
|
||||
setTimeseries(timeseriesResult.buckets);
|
||||
setRecentLogs(logsResult.items);
|
||||
setActiveKeyCount(keysResult.keys.filter((k) => k.is_active).length);
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(load, [load]);
|
||||
|
||||
const instance = me?.odoo_instances?.[0];
|
||||
const requestsUsed = 2431;
|
||||
const requestsLimit = 10000;
|
||||
|
||||
async function rotateLegacyKey() {
|
||||
const key = session.getKey();
|
||||
if (!key) return;
|
||||
if (!confirm('This regenerates your original signup API key. The old key stops working immediately — any client still using it will lose access. Continue?')) return;
|
||||
setRotating(true);
|
||||
try {
|
||||
const result = await api.regenKey(key);
|
||||
session.save(result.api_key, me?.email ?? '');
|
||||
alert('Key rotated. Update any MCP clients using the old key with the new one shown on the API Tokens page.');
|
||||
load();
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Failed to rotate key');
|
||||
} finally {
|
||||
setRotating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl space-y-6">
|
||||
@ -33,142 +78,156 @@ export default function DashboardHome() {
|
||||
<p className="text-sm text-gray-500 mt-0.5">Welcome back{me ? `, ${me.email}` : ''}.</p>
|
||||
</div>
|
||||
|
||||
{/* Stat cards */}
|
||||
{/* Stat cards — all real, from /api/analytics/summary and /api/me */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Subscription"
|
||||
value="Pro Plan"
|
||||
sub="Renews Jul 29, 2026"
|
||||
icon={<span className="text-xl">💎</span>}
|
||||
color="bg-purple-50"
|
||||
label="Requests (30 days)"
|
||||
value={loading ? '—' : (summary?.total_calls ?? 0).toLocaleString()}
|
||||
icon={<Activity size={18} className="text-blue-500" />}
|
||||
color="bg-blue-50"
|
||||
/>
|
||||
<StatCard
|
||||
label="MCP Status"
|
||||
value={<span className="flex items-center gap-1.5"><span className="w-2 h-2 bg-green-400 rounded-full" />Active</span>}
|
||||
sub="All systems operational"
|
||||
icon={<Cpu size={18} className="text-blue-500" />}
|
||||
color="bg-blue-50"
|
||||
label="Successful"
|
||||
value={loading ? '—' : (summary?.success_count ?? 0).toLocaleString()}
|
||||
icon={<CheckCircle2 size={18} className="text-green-500" />}
|
||||
color="bg-green-50"
|
||||
/>
|
||||
<StatCard
|
||||
label="Errors"
|
||||
value={loading ? '—' : (summary?.error_count ?? 0).toLocaleString()}
|
||||
icon={<XCircle size={18} className="text-red-500" />}
|
||||
color="bg-red-50"
|
||||
/>
|
||||
<StatCard
|
||||
label="Odoo Connection"
|
||||
value={instance ? 'Connected' : 'Not connected'}
|
||||
sub={instance ? 'Healthy' : 'Add a connection'}
|
||||
icon={<Globe size={18} className="text-green-500" />}
|
||||
color="bg-green-50"
|
||||
/>
|
||||
<StatCard
|
||||
label="Requests This Month"
|
||||
value={`${requestsUsed.toLocaleString()} / ${requestsLimit.toLocaleString()}`}
|
||||
sub={
|
||||
<div className="mt-1">
|
||||
<div className="h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-brand-500 rounded-full"
|
||||
style={{ width: `${(requestsUsed / requestsLimit) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">{Math.round((requestsUsed / requestsLimit) * 100)}% used</span>
|
||||
</div>
|
||||
}
|
||||
icon={<BarChart2 size={18} className="text-orange-500" />}
|
||||
color="bg-orange-50"
|
||||
icon={<Globe size={18} className="text-purple-500" />}
|
||||
color="bg-purple-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
{/* Odoo Connection Health */}
|
||||
{/* Odoo Connection Health — real data from /api/me */}
|
||||
<div className="lg:col-span-1 bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Odoo Connection Health</h2>
|
||||
{instance ? (
|
||||
<div className="space-y-3 text-xs">
|
||||
<Row label="Odoo URL" value={instance.url} />
|
||||
<Row label="Database" value={instance.database} />
|
||||
<Row label="Last Sync" value="2 minutes ago" />
|
||||
<Row label="Status" value={<span className="text-green-600 font-medium">Healthy</span>} />
|
||||
<button className="w-full text-center text-brand-600 text-xs font-medium border border-brand-200 rounded-lg py-2 hover:bg-brand-50 transition-colors mt-2">
|
||||
View Connection
|
||||
<Row label="Auth Type" value={<span className="capitalize">{instance.type}</span>} />
|
||||
<button
|
||||
onClick={() => navigate('/dashboard/connections')}
|
||||
className="w-full text-center text-brand-600 text-xs font-medium border border-brand-200 rounded-lg py-2 hover:bg-brand-50 transition-colors mt-2"
|
||||
>
|
||||
Manage Connection
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">No Odoo instance connected yet.{' '}
|
||||
<a href="/dashboard/connections" className="text-brand-600 hover:underline">Add one →</a>
|
||||
<button onClick={() => navigate('/dashboard/connections')} className="text-brand-600 hover:underline">
|
||||
Add one →
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{/* Quick Actions — every action is real, no no-ops */}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: 'Copy MCP URL', icon: Copy, action: () => navigator.clipboard.writeText(MCP_URL) },
|
||||
{ label: 'Rotate Token', icon: RotateCcw, action: () => {} },
|
||||
{ label: 'Open Claude Setup', icon: ExternalLink, action: () => {} },
|
||||
{ label: 'Open Codex Setup', icon: ExternalLink, action: () => {} },
|
||||
{ label: 'View Logs', icon: Activity, action: () => {} },
|
||||
].map(({ label, icon: Icon, action }) => (
|
||||
<button key={label} onClick={action}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left">
|
||||
<Icon size={14} className="text-gray-400" /> {label}
|
||||
</button>
|
||||
))}
|
||||
<button onClick={() => navigator.clipboard.writeText(MCP_URL)}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left">
|
||||
<Copy size={14} className="text-gray-400" /> Copy MCP URL
|
||||
</button>
|
||||
<button onClick={() => navigate('/dashboard/tokens')}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left">
|
||||
<Key size={14} className="text-gray-400" /> Manage / Revoke API Keys
|
||||
</button>
|
||||
<button onClick={rotateLegacyKey} disabled={rotating}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left disabled:opacity-50">
|
||||
<RotateCcw size={14} className="text-gray-400" /> {rotating ? 'Rotating…' : 'Rotate Legacy Key'}
|
||||
</button>
|
||||
<button onClick={() => navigate('/dashboard/endpoints')}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left">
|
||||
<ExternalLink size={14} className="text-gray-400" /> MCP Client Setup Guide
|
||||
</button>
|
||||
<button onClick={() => navigate('/dashboard/logs')}
|
||||
className="w-full flex items-center gap-3 text-sm text-gray-700 hover:text-brand-600 hover:bg-brand-50 px-3 py-2 rounded-lg transition-colors text-left">
|
||||
<Cpu size={14} className="text-gray-400" /> View All Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Overview */}
|
||||
{/* Usage Overview — real daily call counts from /api/analytics/timeseries */}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Usage Overview</h2>
|
||||
<div className="flex items-end gap-1 h-24 mb-2">
|
||||
{[40, 65, 45, 80, 55, 90, 70, 85, 60, 95, 75, 100, 60, 85].map((h, i) => (
|
||||
<div key={i} className="flex-1 bg-brand-100 rounded-sm hover:bg-brand-400 transition-colors cursor-pointer"
|
||||
style={{ height: `${h}%` }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>Jun 1</span><span>Jun 15</span><span>Jun 29</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-3">{requestsUsed.toLocaleString()}</p>
|
||||
<p className="text-xs text-gray-500">Requests this month</p>
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4">Usage Overview (30 days)</h2>
|
||||
{timeseries.length === 0 && !loading ? (
|
||||
<p className="text-xs text-gray-400 h-24 flex items-center justify-center">No activity yet.</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={96}>
|
||||
<BarChart data={timeseries}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis dataKey="period" tick={false} axisLine={false} />
|
||||
<YAxis hide />
|
||||
<Tooltip />
|
||||
<Bar dataKey="calls" fill="#c4b5fd" radius={[2, 2, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
<p className="text-2xl font-bold text-gray-900 mt-3">{(summary?.total_calls ?? 0).toLocaleString()}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Requests in the last 30 days · {activeKeyCount ?? '—'} active API key{activeKeyCount === 1 ? '' : 's'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
{/* Recent Activity — real data from /api/logs */}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-gray-900">Recent Activity</h2>
|
||||
<a href="/dashboard/logs" className="text-xs text-brand-600 hover:underline">View all logs →</a>
|
||||
<button onClick={() => navigate('/dashboard/logs')} className="text-xs text-brand-600 hover:underline">
|
||||
View all logs →
|
||||
</button>
|
||||
</div>
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-100">
|
||||
<th className="text-left pb-2 font-medium">Time</th>
|
||||
<th className="text-left pb-2 font-medium">Event</th>
|
||||
<th className="text-left pb-2 font-medium">Status</th>
|
||||
<th className="text-left pb-2 font-medium">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{RECENT_ACTIVITY.map((row, i) => (
|
||||
<tr key={i}>
|
||||
<td className="py-2.5 text-gray-400">{row.time}</td>
|
||||
<td className="py-2.5 text-gray-700">{row.event}</td>
|
||||
<td className="py-2.5">
|
||||
<span className="inline-flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 bg-green-400 rounded-full" />{row.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-500">{row.detail}</td>
|
||||
{recentLogs.length === 0 && !loading ? (
|
||||
<p className="text-xs text-gray-400 text-center py-6">No tool calls yet. Connect an MCP client to get started.</p>
|
||||
) : (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-100">
|
||||
<th className="text-left pb-2 font-medium">Time</th>
|
||||
<th className="text-left pb-2 font-medium">Tool</th>
|
||||
<th className="text-left pb-2 font-medium">Status</th>
|
||||
<th className="text-left pb-2 font-medium">Duration</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{recentLogs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td className="py-2.5 text-gray-400">{log.started_at}</td>
|
||||
<td className="py-2.5 text-gray-700 font-mono">{log.tool_name}</td>
|
||||
<td className="py-2.5">
|
||||
<span className={clsx(
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full',
|
||||
log.success ? 'text-green-600 bg-green-50' : 'text-red-600 bg-red-50',
|
||||
)}>
|
||||
<span className={clsx('w-1.5 h-1.5 rounded-full', log.success ? 'bg-green-400' : 'bg-red-400')} />
|
||||
{log.success ? 'Success' : 'Error'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-500">{log.duration_ms} ms</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, icon, color }: {
|
||||
label: string; value: React.ReactNode; sub: React.ReactNode; icon: React.ReactNode; color: string;
|
||||
function StatCard({ label, value, icon, color }: {
|
||||
label: string; value: React.ReactNode; icon: React.ReactNode; color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-4">
|
||||
@ -177,7 +236,6 @@ function StatCard({ label, value, sub, icon, color }: {
|
||||
<div className={clsx('w-8 h-8 rounded-lg flex items-center justify-center', color)}>{icon}</div>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900">{value}</p>
|
||||
<div className="text-xs text-gray-400 mt-1">{sub}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,23 +2,22 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useLocation, Link, Outlet } from 'react-router-dom';
|
||||
import { session } from '@/lib/api';
|
||||
import {
|
||||
LayoutDashboard, Globe, Cpu, BookOpen, Shield, BarChart2,
|
||||
FileText, CreditCard, Key, Users, Settings, HelpCircle, LogOut, Menu, X,
|
||||
LayoutDashboard, Globe, Cpu, BarChart2, FileText, Key, LogOut, Menu,
|
||||
} from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
// NOTE: AI Setup Guides, Tools & Permissions, Billing, Team, and Settings were
|
||||
// removed from this nav because no backend feature exists for them yet
|
||||
// (no team/multi-user system, no per-tool permission toggles, no real
|
||||
// subscription/billing system). See FUTURE_FEATURES.md for what to rebuild
|
||||
// and how, once those backend features are designed.
|
||||
const NAV = [
|
||||
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ label: 'Odoo Connections', href: '/dashboard/connections', icon: Globe },
|
||||
{ label: 'MCP Endpoints', href: '/dashboard/endpoints', icon: Cpu },
|
||||
{ label: 'AI Setup Guides', href: '/dashboard/guides', icon: BookOpen },
|
||||
{ label: 'Tools & Permissions', href: '/dashboard/tools', icon: Shield },
|
||||
{ label: 'Usage', href: '/dashboard/usage', icon: BarChart2 },
|
||||
{ label: 'Logs', href: '/dashboard/logs', icon: FileText },
|
||||
{ label: 'Billing', href: '/dashboard/billing', icon: CreditCard },
|
||||
{ label: 'API Tokens', href: '/dashboard/tokens', icon: Key },
|
||||
{ label: 'Team', href: '/dashboard/team', icon: Users },
|
||||
{ label: 'Settings', href: '/dashboard/settings', icon: Settings },
|
||||
];
|
||||
|
||||
export default function DashboardLayout() {
|
||||
@ -77,10 +76,6 @@ export default function DashboardLayout() {
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="px-3 pb-4 border-t border-gray-800 pt-3 space-y-0.5">
|
||||
<Link to="/dashboard/support"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm hover:bg-gray-800 hover:text-white transition-colors">
|
||||
<HelpCircle size={16} /> Support
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm hover:bg-gray-800 hover:text-white transition-colors">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user