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 DashboardLayout from './pages/dashboard/DashboardLayout';
|
||||||
import DashboardHome from './pages/dashboard/DashboardHome';
|
import DashboardHome from './pages/dashboard/DashboardHome';
|
||||||
import Connections from './pages/dashboard/Connections';
|
import Connections from './pages/dashboard/Connections';
|
||||||
import Billing from './pages/dashboard/Billing';
|
|
||||||
import Endpoints from './pages/dashboard/Endpoints';
|
import Endpoints from './pages/dashboard/Endpoints';
|
||||||
import Tokens from './pages/dashboard/Tokens';
|
import Tokens from './pages/dashboard/Tokens';
|
||||||
import Usage from './pages/dashboard/Usage';
|
import Usage from './pages/dashboard/Usage';
|
||||||
@ -33,7 +32,6 @@ export default function App() {
|
|||||||
<Route path="/dashboard" element={<DashboardLayout />}>
|
<Route path="/dashboard" element={<DashboardLayout />}>
|
||||||
<Route index element={<DashboardHome />} />
|
<Route index element={<DashboardHome />} />
|
||||||
<Route path="connections" element={<Connections />} />
|
<Route path="connections" element={<Connections />} />
|
||||||
<Route path="billing" element={<Billing />} />
|
|
||||||
<Route path="endpoints" element={<Endpoints />} />
|
<Route path="endpoints" element={<Endpoints />} />
|
||||||
<Route path="tokens" element={<Tokens />} />
|
<Route path="tokens" element={<Tokens />} />
|
||||||
<Route path="usage" element={<Usage />} />
|
<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 { useEffect, useState, useCallback } from 'react';
|
||||||
import { api, session, type MeResult } from '@/lib/api';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Cpu, Globe, BarChart2, Copy, RotateCcw, ExternalLink, Activity } from 'lucide-react';
|
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';
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
const MCP_URL = `${import.meta.env.VITE_API_URL ?? 'https://odoo-mcp.thedomainnest.com'}/mcp/sse`;
|
const MCP_URL = `${import.meta.env.VITE_API_URL ?? 'https://odoo-mcp.thedomainnest.com'}/mcp/sse`;
|
||||||
|
|
||||||
const RECENT_ACTIVITY = [
|
function isoDaysAgo(days: number): string {
|
||||||
{ time: '2 mins ago', event: 'Connection sync', status: 'Success', detail: 'Odoo connection is healthy' },
|
const d = new Date();
|
||||||
{ time: '5 mins ago', event: 'MCP request', status: 'Success', detail: 'search_invoices' },
|
d.setUTCDate(d.getUTCDate() - days);
|
||||||
{ time: '12 mins ago', event: 'Token rotated', status: 'Success', detail: 'New token generated' },
|
return d.toISOString();
|
||||||
{ time: '1 hour ago', event: 'Connection sync', status: 'Success', detail: 'Odoo connection is healthy' },
|
}
|
||||||
];
|
|
||||||
|
|
||||||
export default function DashboardHome() {
|
export default function DashboardHome() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [me, setMe] = useState<MeResult | null>(null);
|
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 [loading, setLoading] = useState(true);
|
||||||
|
const [rotating, setRotating] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const load = useCallback(() => {
|
||||||
const key = session.getKey();
|
const key = session.getKey();
|
||||||
if (!key) return;
|
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 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 (
|
return (
|
||||||
<div className="max-w-6xl space-y-6">
|
<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>
|
<p className="text-sm text-gray-500 mt-0.5">Welcome back{me ? `, ${me.email}` : ''}.</p>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Subscription"
|
label="Requests (30 days)"
|
||||||
value="Pro Plan"
|
value={loading ? '—' : (summary?.total_calls ?? 0).toLocaleString()}
|
||||||
sub="Renews Jul 29, 2026"
|
icon={<Activity size={18} className="text-blue-500" />}
|
||||||
icon={<span className="text-xl">💎</span>}
|
color="bg-blue-50"
|
||||||
color="bg-purple-50"
|
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="MCP Status"
|
label="Successful"
|
||||||
value={<span className="flex items-center gap-1.5"><span className="w-2 h-2 bg-green-400 rounded-full" />Active</span>}
|
value={loading ? '—' : (summary?.success_count ?? 0).toLocaleString()}
|
||||||
sub="All systems operational"
|
icon={<CheckCircle2 size={18} className="text-green-500" />}
|
||||||
icon={<Cpu size={18} className="text-blue-500" />}
|
color="bg-green-50"
|
||||||
color="bg-blue-50"
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Errors"
|
||||||
|
value={loading ? '—' : (summary?.error_count ?? 0).toLocaleString()}
|
||||||
|
icon={<XCircle size={18} className="text-red-500" />}
|
||||||
|
color="bg-red-50"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Odoo Connection"
|
label="Odoo Connection"
|
||||||
value={instance ? 'Connected' : 'Not connected'}
|
value={instance ? 'Connected' : 'Not connected'}
|
||||||
sub={instance ? 'Healthy' : 'Add a connection'}
|
icon={<Globe size={18} className="text-purple-500" />}
|
||||||
icon={<Globe size={18} className="text-green-500" />}
|
color="bg-purple-50"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<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">
|
<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>
|
<h2 className="text-sm font-semibold text-gray-900 mb-4">Odoo Connection Health</h2>
|
||||||
{instance ? (
|
{instance ? (
|
||||||
<div className="space-y-3 text-xs">
|
<div className="space-y-3 text-xs">
|
||||||
<Row label="Odoo URL" value={instance.url} />
|
<Row label="Odoo URL" value={instance.url} />
|
||||||
<Row label="Database" value={instance.database} />
|
<Row label="Database" value={instance.database} />
|
||||||
<Row label="Last Sync" value="2 minutes ago" />
|
<Row label="Auth Type" value={<span className="capitalize">{instance.type}</span>} />
|
||||||
<Row label="Status" value={<span className="text-green-600 font-medium">Healthy</span>} />
|
<button
|
||||||
<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">
|
onClick={() => navigate('/dashboard/connections')}
|
||||||
View Connection
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-500">No Odoo instance connected yet.{' '}
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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>
|
<h2 className="text-sm font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[
|
<button onClick={() => navigator.clipboard.writeText(MCP_URL)}
|
||||||
{ label: 'Copy MCP URL', icon: Copy, action: () => 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">
|
||||||
{ label: 'Rotate Token', icon: RotateCcw, action: () => {} },
|
<Copy size={14} className="text-gray-400" /> Copy MCP URL
|
||||||
{ label: 'Open Claude Setup', icon: ExternalLink, action: () => {} },
|
</button>
|
||||||
{ label: 'Open Codex Setup', icon: ExternalLink, action: () => {} },
|
<button onClick={() => navigate('/dashboard/tokens')}
|
||||||
{ label: 'View Logs', icon: Activity, 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">
|
||||||
].map(({ label, icon: Icon, action }) => (
|
<Key size={14} className="text-gray-400" /> Manage / Revoke API Keys
|
||||||
<button key={label} onClick={action}
|
</button>
|
||||||
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">
|
<button onClick={rotateLegacyKey} disabled={rotating}
|
||||||
<Icon size={14} className="text-gray-400" /> {label}
|
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">
|
||||||
</button>
|
<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>
|
||||||
</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">
|
<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>
|
<h2 className="text-sm font-semibold text-gray-900 mb-4">Usage Overview (30 days)</h2>
|
||||||
<div className="flex items-end gap-1 h-24 mb-2">
|
{timeseries.length === 0 && !loading ? (
|
||||||
{[40, 65, 45, 80, 55, 90, 70, 85, 60, 95, 75, 100, 60, 85].map((h, i) => (
|
<p className="text-xs text-gray-400 h-24 flex items-center justify-center">No activity yet.</p>
|
||||||
<div key={i} className="flex-1 bg-brand-100 rounded-sm hover:bg-brand-400 transition-colors cursor-pointer"
|
) : (
|
||||||
style={{ height: `${h}%` }} />
|
<ResponsiveContainer width="100%" height={96}>
|
||||||
))}
|
<BarChart data={timeseries}>
|
||||||
</div>
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
<XAxis dataKey="period" tick={false} axisLine={false} />
|
||||||
<span>Jun 1</span><span>Jun 15</span><span>Jun 29</span>
|
<YAxis hide />
|
||||||
</div>
|
<Tooltip />
|
||||||
<p className="text-2xl font-bold text-gray-900 mt-3">{requestsUsed.toLocaleString()}</p>
|
<Bar dataKey="calls" fill="#c4b5fd" radius={[2, 2, 0, 0]} />
|
||||||
<p className="text-xs text-gray-500">Requests this month</p>
|
</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>
|
||||||
</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="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-sm font-semibold text-gray-900">Recent Activity</h2>
|
<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>
|
</div>
|
||||||
<table className="w-full text-xs">
|
{recentLogs.length === 0 && !loading ? (
|
||||||
<thead>
|
<p className="text-xs text-gray-400 text-center py-6">No tool calls yet. Connect an MCP client to get started.</p>
|
||||||
<tr className="text-gray-400 border-b border-gray-100">
|
) : (
|
||||||
<th className="text-left pb-2 font-medium">Time</th>
|
<table className="w-full text-xs">
|
||||||
<th className="text-left pb-2 font-medium">Event</th>
|
<thead>
|
||||||
<th className="text-left pb-2 font-medium">Status</th>
|
<tr className="text-gray-400 border-b border-gray-100">
|
||||||
<th className="text-left pb-2 font-medium">Details</th>
|
<th className="text-left pb-2 font-medium">Time</th>
|
||||||
</tr>
|
<th className="text-left pb-2 font-medium">Tool</th>
|
||||||
</thead>
|
<th className="text-left pb-2 font-medium">Status</th>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<th className="text-left pb-2 font-medium">Duration</th>
|
||||||
{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>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="divide-y divide-gray-50">
|
||||||
</table>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ label, value, sub, icon, color }: {
|
function StatCard({ label, value, icon, color }: {
|
||||||
label: string; value: React.ReactNode; sub: React.ReactNode; icon: React.ReactNode; color: string;
|
label: string; value: React.ReactNode; icon: React.ReactNode; color: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-4">
|
<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 className={clsx('w-8 h-8 rounded-lg flex items-center justify-center', color)}>{icon}</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-gray-900">{value}</p>
|
<p className="text-sm font-semibold text-gray-900">{value}</p>
|
||||||
<div className="text-xs text-gray-400 mt-1">{sub}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,23 +2,22 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate, useLocation, Link, Outlet } from 'react-router-dom';
|
import { useNavigate, useLocation, Link, Outlet } from 'react-router-dom';
|
||||||
import { session } from '@/lib/api';
|
import { session } from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, Globe, Cpu, BookOpen, Shield, BarChart2,
|
LayoutDashboard, Globe, Cpu, BarChart2, FileText, Key, LogOut, Menu,
|
||||||
FileText, CreditCard, Key, Users, Settings, HelpCircle, LogOut, Menu, X,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
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 = [
|
const NAV = [
|
||||||
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
{ label: 'Odoo Connections', href: '/dashboard/connections', icon: Globe },
|
{ label: 'Odoo Connections', href: '/dashboard/connections', icon: Globe },
|
||||||
{ label: 'MCP Endpoints', href: '/dashboard/endpoints', icon: Cpu },
|
{ 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: 'Usage', href: '/dashboard/usage', icon: BarChart2 },
|
||||||
{ label: 'Logs', href: '/dashboard/logs', icon: FileText },
|
{ label: 'Logs', href: '/dashboard/logs', icon: FileText },
|
||||||
{ label: 'Billing', href: '/dashboard/billing', icon: CreditCard },
|
|
||||||
{ label: 'API Tokens', href: '/dashboard/tokens', icon: Key },
|
{ 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() {
|
export default function DashboardLayout() {
|
||||||
@ -77,10 +76,6 @@ export default function DashboardLayout() {
|
|||||||
|
|
||||||
{/* Bottom */}
|
{/* Bottom */}
|
||||||
<div className="px-3 pb-4 border-t border-gray-800 pt-3 space-y-0.5">
|
<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
|
<button
|
||||||
onClick={logout}
|
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">
|
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