Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.next/
node_modules/
coverage/
out/
build/
*.tsbuildinfo
next-env.d.ts
11 changes: 11 additions & 0 deletions apps/dashboard/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
]);

export default eslintConfig;
10 changes: 10 additions & 0 deletions apps/dashboard/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
compress: true,
poweredByHeader: false,

serverExternalPackages: ['better-sqlite3'],
};

export default nextConfig;
46 changes: 46 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "codingbuddy-dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"lint:fix": "eslint --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"validate": "yarn lint && yarn format:check && yarn typecheck && yarn test"
},
"dependencies": {
"better-sqlite3": "^11.9.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"recharts": "^2.15.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5.1.3",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"happy-dom": "^20.5.0",
"prettier": "^3.4.2",
"tailwindcss": "^4",
"typescript": "^5",
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}
7 changes: 7 additions & 0 deletions apps/dashboard/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};

export default config;
21 changes: 21 additions & 0 deletions apps/dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import 'tailwindcss';

:root {
--color-bg: #0a0a0f;
--color-surface: #12121a;
--color-surface-hover: #1a1a26;
--color-border: #1e1e2e;
--color-text: #e4e4ef;
--color-text-muted: #8888a0;
--color-accent: #6366f1;
--color-accent-light: #818cf8;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
}

body {
background: var(--color-bg);
color: var(--color-text);
font-family: system-ui, -apple-system, sans-serif;
}
19 changes: 19 additions & 0 deletions apps/dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
title: 'Codingbuddy Dashboard',
description: 'Execution history, cost tracking, and agent activity dashboard',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen antialiased">{children}</body>
</html>
);
}
39 changes: 39 additions & 0 deletions apps/dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
loadSessions,
loadCostEntries,
loadAgentActivity,
loadSkillUsage,
loadPREntries,
} from '@/lib/data-loader';
import type { DashboardData } from '@/lib/types';
import { DashboardContent } from '@/components/dashboard-content';

export const dynamic = 'force-dynamic';

export default async function DashboardPage() {
const [sessions, costEntries, agentActivity, skillUsage, prEntries] =
await Promise.all([
loadSessions(),
loadCostEntries(),
loadAgentActivity(),
loadSkillUsage(),
loadPREntries(),
]);

const isUsingMockData = sessions.length > 0 && sessions[0].sessionId.startsWith('session-');

const data: DashboardData = {
sessions,
costEntries,
agentActivity,
skillUsage,
prEntries,
isUsingMockData,
};

return (
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<DashboardContent data={data} />
</main>
);
}
69 changes: 69 additions & 0 deletions apps/dashboard/src/components/agent-activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client';

import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from 'recharts';
import type { AgentActivity as AgentActivityType } from '@/lib/types';

interface AgentActivityProps {
agents: AgentActivityType[];
}

const COLORS = [
'#6366f1',
'#818cf8',
'#a78bfa',
'#c084fc',
'#e879f9',
'#f472b6',
'#fb7185',
'#f87171',
];

export function AgentActivity({ agents }: AgentActivityProps) {
const top8 = agents.slice(0, 8);

return (
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-5">
<h2 className="mb-4 text-lg font-semibold">Agent Activity</h2>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={top8} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
<XAxis
type="number"
tick={{ fill: 'var(--color-text-muted)', fontSize: 11 }}
/>
<YAxis
type="category"
dataKey="agent"
width={140}
tick={{ fill: 'var(--color-text-muted)', fontSize: 11 }}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
color: 'var(--color-text)',
}}
formatter={(value: number) => [`${value} calls`, 'Usage']}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
{top8.map((_, index) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
}
72 changes: 72 additions & 0 deletions apps/dashboard/src/components/cost-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import type { CostEntry } from '@/lib/types';

interface CostChartProps {
entries: CostEntry[];
}

export function CostChart({ entries }: CostChartProps) {
const cumulative = entries.reduce<(CostEntry & { cumCost: number })[]>(
(acc, entry) => {
const prev = acc.length > 0 ? acc[acc.length - 1].cumCost : 0;
acc.push({ ...entry, cumCost: parseFloat((prev + entry.cost).toFixed(2)) });
return acc;
},
[]
);

return (
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-5">
<h2 className="mb-4 text-lg font-semibold">Cumulative Cost</h2>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={cumulative}>
<defs>
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-accent)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--color-accent)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
<XAxis
dataKey="date"
tick={{ fill: 'var(--color-text-muted)', fontSize: 11 }}
tickFormatter={(v: string) => v.slice(5)}
/>
<YAxis
tick={{ fill: 'var(--color-text-muted)', fontSize: 11 }}
tickFormatter={(v: number) => `$${v}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
color: 'var(--color-text)',
}}
formatter={(value: number) => [`$${value.toFixed(2)}`, 'Cumulative Cost']}
labelFormatter={(label: string) => `Date: ${label}`}
/>
<Area
type="monotone"
dataKey="cumCost"
stroke="var(--color-accent)"
fill="url(#costGradient)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions apps/dashboard/src/components/dashboard-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import type { DashboardData } from '@/lib/types';
import { DashboardHeader } from './dashboard-header';
import { SessionTimeline } from './session-timeline';
import { CostChart } from './cost-chart';
import { AgentActivity } from './agent-activity';
import { SkillUsage } from './skill-usage';
import { PRThroughput } from './pr-throughput';

interface DashboardContentProps {
data: DashboardData;
}

export function DashboardContent({ data }: DashboardContentProps) {
return (
<>
<DashboardHeader data={data} />

<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<CostChart entries={data.costEntries} />
<PRThroughput entries={data.prEntries} />
<AgentActivity agents={data.agentActivity} />
<SkillUsage skills={data.skillUsage} />
</div>

<div className="mt-6">
<SessionTimeline sessions={data.sessions} />
</div>
</>
);
}
Loading