feat(web): admin panel with session management (#89)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #89.
This commit is contained in:
99
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
99
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
|
interface SessionInfo {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
modelId: string;
|
||||||
|
createdAt: string;
|
||||||
|
promptCount: number;
|
||||||
|
channels: string[];
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionsResponse {
|
||||||
|
sessions: SessionInfo[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPage(): React.ReactElement {
|
||||||
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api<SessionsResponse>('/api/sessions')
|
||||||
|
.then((res) => setSessions(res.sessions))
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-4xl space-y-8">
|
||||||
|
<h1 className="text-2xl font-semibold">Admin</h1>
|
||||||
|
|
||||||
|
{/* User Management placeholder */}
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-4 text-lg font-medium text-text-secondary">User Management</h2>
|
||||||
|
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
User management will be available when the admin API is implemented
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Active Agent Sessions */}
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-4 text-lg font-medium text-text-secondary">Active Agent Sessions</h2>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-text-muted">Loading sessions...</p>
|
||||||
|
) : sessions.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||||
|
<p className="text-sm text-text-muted">No active sessions</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden rounded-lg border border-surface-border">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
||||||
|
<th className="px-4 py-2 font-medium">Session ID</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Provider</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Model</th>
|
||||||
|
<th className="hidden px-4 py-2 font-medium md:table-cell">Prompts</th>
|
||||||
|
<th className="hidden px-4 py-2 font-medium md:table-cell">Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sessions.map((s) => (
|
||||||
|
<tr key={s.id} className="border-b border-surface-border last:border-b-0">
|
||||||
|
<td className="max-w-[200px] truncate px-4 py-2 font-mono text-xs text-text-primary">
|
||||||
|
{s.id}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-xs text-text-muted">{s.provider}</td>
|
||||||
|
<td className="px-4 py-2 text-xs text-text-muted">{s.modelId}</td>
|
||||||
|
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||||
|
{s.promptCount}
|
||||||
|
</td>
|
||||||
|
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||||
|
{formatDuration(s.durationMs)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
return `${hours}h ${minutes % 60}m`;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ const navItems: NavItem[] = [
|
|||||||
{ label: 'Tasks', href: '/tasks', icon: '📋' },
|
{ label: 'Tasks', href: '/tasks', icon: '📋' },
|
||||||
{ label: 'Projects', href: '/projects', icon: '📁' },
|
{ label: 'Projects', href: '/projects', icon: '📁' },
|
||||||
{ label: 'Settings', href: '/settings', icon: '⚙️' },
|
{ label: 'Settings', href: '/settings', icon: '⚙️' },
|
||||||
|
{ label: 'Admin', href: '/admin', icon: '🛡️' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar(): React.ReactElement {
|
export function Sidebar(): React.ReactElement {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
||||||
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
||||||
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
||||||
| P3-006 | in-progress | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
|
| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 |
|
||||||
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
|
| P3-007 | in-progress | Phase 3 | Admin panel — user management, RBAC | — | #32 |
|
||||||
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||||
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||||
| P4-002 | not-started | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
| P4-002 | not-started | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||||
|
|||||||
Reference in New Issue
Block a user