agentfit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
@@ -0,0 +1,111 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ build-mac:
13
+ runs-on: macos-latest
14
+ timeout-minutes: 30
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+
18
+ - uses: actions/setup-node@v6
19
+ with:
20
+ node-version: 22
21
+ cache: npm
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Import signing certificate
27
+ env:
28
+ CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
29
+ CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
30
+ run: |
31
+ echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
32
+ security create-keychain -p actions build.keychain
33
+ security default-keychain -s build.keychain
34
+ security unlock-keychain -p actions build.keychain
35
+ security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
36
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions build.keychain
37
+ rm certificate.p12
38
+
39
+ - name: Build macOS DMGs
40
+ run: npm run electron:build:mac
41
+
42
+ - name: Upload DMGs
43
+ uses: actions/upload-artifact@v7
44
+ with:
45
+ name: mac-dmgs
46
+ path: |
47
+ dist-electron/*.dmg
48
+
49
+ build-win:
50
+ runs-on: windows-latest
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+
54
+ - uses: actions/setup-node@v6
55
+ with:
56
+ node-version: 22
57
+ cache: npm
58
+
59
+ - name: Install dependencies
60
+ run: npm ci
61
+
62
+ - name: Build Windows installer
63
+ run: npm run electron:build:win
64
+
65
+ - name: Upload exe
66
+ uses: actions/upload-artifact@v7
67
+ with:
68
+ name: win-exe
69
+ path: |
70
+ dist-electron/*.exe
71
+
72
+ publish-npm:
73
+ runs-on: ubuntu-latest
74
+ steps:
75
+ - uses: actions/checkout@v6
76
+
77
+ - uses: actions/setup-node@v6
78
+ with:
79
+ node-version: 22
80
+ registry-url: https://registry.npmjs.org
81
+
82
+ - name: Install dependencies
83
+ run: npm ci
84
+
85
+ - name: Build
86
+ run: npm run build
87
+
88
+ - name: Publish to npm
89
+ run: npm publish
90
+ env:
91
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
92
+
93
+ release:
94
+ needs: [build-mac, build-win]
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/download-artifact@v8
98
+ with:
99
+ name: mac-dmgs
100
+ path: artifacts
101
+
102
+ - uses: actions/download-artifact@v8
103
+ with:
104
+ name: win-exe
105
+ path: artifacts
106
+
107
+ - name: Create GitHub Release
108
+ uses: softprops/action-gh-release@v2
109
+ with:
110
+ files: artifacts/*
111
+ generate_release_notes: true
package/README.md CHANGED
@@ -4,64 +4,79 @@ Fitness tracker dashboard for AI coding agents. Reads your local Claude Code and
4
4
 
5
5
  ## Install
6
6
 
7
- ### Option 1: One-liner (recommended)
7
+ ### Option 1: Desktop App (recommended)
8
8
 
9
- ```bash
10
- curl -fsSL https://raw.githubusercontent.com/harrywang/agentfit/main/setup.sh | bash
11
- ```
9
+ Download a pre-built installer from the [Releases](https://github.com/harrywang/agentfit/releases) page:
12
10
 
13
- Then start the dashboard:
11
+ - **macOS**: `AgentFit-x.x.x.dmg` (Intel) or `AgentFit-x.x.x-arm64.dmg` (Apple Silicon)
12
+ - **Windows**: `AgentFit-x.x.x.exe`
13
+
14
+ > **macOS note:** If you see "AgentFit Not Opened" on first launch, go to **System Settings > Privacy & Security**, scroll down, and click **Open Anyway**. Or run `xattr -cr /Applications/AgentFit.app` in Terminal.
15
+
16
+ ### Option 2: One-liner
14
17
 
15
18
  ```bash
16
- cd agentfit && npm start
19
+ curl -fsSL https://raw.githubusercontent.com/harrywang/agentfit/main/setup.sh | bash
17
20
  ```
18
21
 
19
- ### Option 2: npx
22
+ ### Option 3: npx
20
23
 
21
24
  ```bash
22
25
  npx agentfit
23
26
  ```
24
27
 
25
- This handles first-run setup (database creation, build) automatically.
26
-
27
- ### Option 3: Manual
28
+ ### Option 4: Manual
28
29
 
29
30
  ```bash
30
31
  git clone https://github.com/harrywang/agentfit.git
31
32
  cd agentfit
32
33
  npm install
33
- echo 'DATABASE_URL="file:./dev.db"' > .env
34
+ echo 'DATABASE_URL="file:./agentfit.db"' > .env
34
35
  npx prisma migrate deploy
35
36
  npm run build
36
37
  npm start
37
38
  ```
38
39
 
39
- Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your `~/.claude/projects/` logs on first load.
40
+ Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your Claude Code (`~/.claude/projects/`) and Codex (`~/.codex/sessions/`) logs on first load.
41
+
42
+ **Requirements:** Node.js 20+ (Options 2–4)
43
+
44
+ ## The CRAFT Framework
45
+
46
+ AgentFit scores your AI coding proficiency using **CRAFT** — a Human-AI coding proficiency framework by [Harry Wang](https://harrywang.me). All metrics are computed from your local conversation logs.
47
+
48
+ | | Dimension | What it measures | Key metrics |
49
+ |---|---|---|---|
50
+ | **C** | **Context** | How effectively you engineer context for the AI | CLAUDE.md usage, memory writes, cache hit rate |
51
+ | **R** | **Reach** | How broadly you leverage available capabilities | Tool diversity, subagent usage, skill adoption |
52
+ | **A** | **Autonomy** | How independently the agent works for you | Message ratio, interruption rate, delegation |
53
+ | **F** | **Flow** | How consistently you maintain a coding rhythm | Streak length, daily consistency, active days |
54
+ | **T** | **Throughput** | How much output you get for your investment | Cost efficiency, output volume, error rate |
40
55
 
41
- **Requirements:** Node.js 20+
56
+ Inspired by [DORA Metrics](https://dora.dev) and Microsoft's [SPACE framework](https://queue.acm.org/detail.cfm?id=3454124). Behavioral signals from your logs — no surveys, no guesswork. Each dimension is scored 0–100.
42
57
 
43
58
  ## Features
44
59
 
45
60
  - **Dashboard** — overview stats (cost, tokens, sessions, projects, messages, tool calls, duration)
46
- - **Agent Coach** — fitness score, achievements, improvement areas, and tips
61
+ - **CRAFT Coach** — fitness score, achievements, and actionable improvement tips
47
62
  - **Daily Usage** — daily cost and activity charts
48
63
  - **Token Breakdown** — pie chart + stacked area chart of token types
49
- - **Tool Usage** — top 20 tools by invocation count
64
+ - **Tool Usage** — top tools by invocation count
50
65
  - **Projects** — per-project breakdown with top tools and sessions
51
- - **Personality Fit** — MBTI-style behavioral analysis with task fit scores
66
+ - **Sessions** — individual session details with chat logs and tool flow graphs
67
+ - **Personality Fit** — MBTI-style behavioral analysis
52
68
  - **Command Usage** — slash command pattern tracking
53
69
  - **Images** — screenshot analysis across sessions
54
- - **Community Plugins** — extensible analysis views contributed by the community
70
+ - **Community Plugins** — extensible analysis views
55
71
 
56
- ## Data
72
+ ## Development
57
73
 
58
- - Session metrics stored in local SQLite (`dev.db`) via Prisma
59
- - Images extracted from conversation logs to `data/images/`
60
- - Pricing fetched from [LiteLLM](https://github.com/BerriAI/litellm) model pricing database
61
- - Incremental sync — only new sessions are imported on each sync
62
- - Supports Claude Code, Codex, or combined views
74
+ Build the desktop app locally:
63
75
 
64
- ## Development
76
+ ```bash
77
+ npm run electron:build:mac # Mac (.dmg)
78
+ npm run electron:build:win # Windows (.exe)
79
+ ```
65
80
 
66
81
  ```bash
67
82
  npm run dev # Start dev server (Turbopack)
@@ -70,23 +85,11 @@ npm run lint # ESLint
70
85
  npm run format # Prettier
71
86
  npm run typecheck # TypeScript check
72
87
  npm test # Run tests
73
- npm run test:watch # Tests in watch mode
74
- ```
75
-
76
- ### Database
77
-
78
- ```bash
79
- npx prisma migrate dev --name <name> # Create/apply migration
80
- npx prisma generate # Regenerate Prisma client
81
88
  ```
82
89
 
83
90
  ## Community Plugins
84
91
 
85
- AgentFit has a plugin system for community-contributed analysis views. Plugins appear in the **Community** section of the sidebar.
86
-
87
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own plugin.
88
-
89
- **Quick version:**
92
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick version:
90
93
 
91
94
  1. Create `plugins/<your-slug>/manifest.ts` and `component.tsx`
92
95
  2. Register in `plugins/index.ts`
@@ -98,7 +101,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own p
98
101
  | Variable | Default | Description |
99
102
  |----------|---------|-------------|
100
103
  | `PORT` or `AGENTFIT_PORT` | `3000` | Server port |
101
- | `DATABASE_URL` | `file:./dev.db` | SQLite database path |
104
+ | `DATABASE_URL` | `file:./agentfit.db` | SQLite database path |
102
105
 
103
106
  ## Credits
104
107
 
@@ -11,7 +11,7 @@ export default function DailyPage() {
11
11
  return (
12
12
  <>
13
13
  <div className="grid gap-4 lg:grid-cols-3">
14
- <DailyChart daily={data.daily} />
14
+ <DailyChart daily={data.daily} sessions={data.sessions} />
15
15
  </div>
16
16
  <DailyTable daily={data.daily} sessions={data.sessions} />
17
17
  </>
@@ -0,0 +1,180 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useData } from '@/components/data-provider'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '@/components/ui/dialog'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import {
16
+ RefreshCw,
17
+ RotateCcw,
18
+ AlertTriangle,
19
+ Database,
20
+ HardDrive,
21
+ FolderSync,
22
+ } from 'lucide-react'
23
+ import { BackupCard } from '@/components/backup-section'
24
+
25
+ export default function SettingsPage() {
26
+ const { syncing, resetting, lastSyncResult, lastSyncTime, handleSync, handleReset } = useData()
27
+ const [resetOpen, setResetOpen] = useState(false)
28
+ const [paths, setPaths] = useState<{ database: string; images: string; claudeLogs: string; codexLogs: string } | null>(null)
29
+
30
+ useEffect(() => {
31
+ fetch('/api/check')
32
+ .then((r) => r.json())
33
+ .then((d) => { if (d.paths) setPaths(d.paths) })
34
+ .catch(() => {})
35
+ }, [])
36
+
37
+ return (
38
+ <div className="space-y-6 max-w-2xl">
39
+ {/* Local Data */}
40
+ <Card>
41
+ <CardHeader>
42
+ <div className="flex items-center gap-2">
43
+ <Database className="h-5 w-5 text-muted-foreground" />
44
+ <div>
45
+ <CardTitle>Local Data</CardTitle>
46
+ <CardDescription>Manage your synced session data</CardDescription>
47
+ </div>
48
+ </div>
49
+ </CardHeader>
50
+ <CardContent className="space-y-4">
51
+ {/* Sync */}
52
+ <div className="flex items-center justify-between rounded-lg border p-4">
53
+ <div className="space-y-1">
54
+ <div className="flex items-center gap-2">
55
+ <FolderSync className="h-4 w-4 text-muted-foreground" />
56
+ <span className="text-sm font-medium">Sync Logs</span>
57
+ </div>
58
+ <p className="text-xs text-muted-foreground">
59
+ Import new sessions from ~/.claude and ~/.codex into the local database.
60
+ </p>
61
+ {lastSyncTime && (
62
+ <p className="text-xs text-muted-foreground">
63
+ Last synced: {lastSyncTime.toLocaleString()}
64
+ {lastSyncResult && (
65
+ <span> — {lastSyncResult.sessionsAdded} added, {lastSyncResult.sessionsSkipped} skipped</span>
66
+ )}
67
+ </p>
68
+ )}
69
+ </div>
70
+ <Button
71
+ variant="outline"
72
+ onClick={handleSync}
73
+ disabled={syncing || resetting}
74
+ className="gap-2 shrink-0"
75
+ >
76
+ <RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
77
+ {syncing ? 'Syncing...' : 'Sync'}
78
+ </Button>
79
+ </div>
80
+
81
+ {/* Reset */}
82
+ <div className="flex items-center justify-between rounded-lg border border-destructive/20 p-4">
83
+ <div className="space-y-1">
84
+ <div className="flex items-center gap-2">
85
+ <RotateCcw className="h-4 w-4 text-destructive" />
86
+ <span className="text-sm font-medium text-destructive">Reset Database</span>
87
+ </div>
88
+ <p className="text-xs text-muted-foreground">
89
+ Delete all synced data and re-import from log files on disk. Use if data seems corrupted.
90
+ </p>
91
+ </div>
92
+ <Button
93
+ variant="destructive"
94
+ onClick={() => setResetOpen(true)}
95
+ disabled={syncing || resetting}
96
+ className="gap-2 shrink-0"
97
+ >
98
+ <RotateCcw className={`h-4 w-4 ${resetting ? 'animate-spin' : ''}`} />
99
+ {resetting ? 'Resetting...' : 'Reset'}
100
+ </Button>
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+
105
+ {/* Database Info */}
106
+ <Card>
107
+ <CardHeader>
108
+ <div className="flex items-center gap-2">
109
+ <HardDrive className="h-5 w-5 text-muted-foreground" />
110
+ <div>
111
+ <CardTitle>Storage</CardTitle>
112
+ <CardDescription>Where your data lives</CardDescription>
113
+ </div>
114
+ </div>
115
+ </CardHeader>
116
+ <CardContent>
117
+ <div className="space-y-3 text-sm">
118
+ <div className="flex items-center justify-between">
119
+ <span className="text-muted-foreground">Database</span>
120
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.database ?? 'agentfit.db'}</code>
121
+ </div>
122
+ <div className="flex items-center justify-between">
123
+ <span className="text-muted-foreground">Claude Code logs</span>
124
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.claudeLogs ?? '~/.claude/projects/'}</code>
125
+ </div>
126
+ <div className="flex items-center justify-between">
127
+ <span className="text-muted-foreground">Codex logs</span>
128
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.codexLogs ?? '~/.codex/sessions/'}</code>
129
+ </div>
130
+ <div className="flex items-center justify-between">
131
+ <span className="text-muted-foreground">Extracted images</span>
132
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.images ?? 'data/images/'}</code>
133
+ </div>
134
+ </div>
135
+ </CardContent>
136
+ </Card>
137
+
138
+ {/* GitHub Backup */}
139
+ <BackupCard />
140
+
141
+ {/* Reset confirmation dialog */}
142
+ <Dialog open={resetOpen} onOpenChange={setResetOpen}>
143
+ <DialogContent className="sm:max-w-md">
144
+ <DialogHeader>
145
+ <DialogTitle className="flex items-center gap-2 text-destructive">
146
+ <AlertTriangle className="h-5 w-5" />
147
+ Destructive Action
148
+ </DialogTitle>
149
+ </DialogHeader>
150
+ <div className="space-y-3 text-sm text-muted-foreground">
151
+ <div>
152
+ This will <strong className="text-foreground">permanently delete all synced data</strong> from the database and re-import from log files currently on disk.
153
+ </div>
154
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
155
+ <strong>Warning:</strong> If your coding agent (Claude Code, Codex, etc.) has purged old log files, that data will be permanently lost. The database may contain sessions that no longer exist on disk.
156
+ </div>
157
+ <div>
158
+ This cannot be undone. Consider backing up <code className="rounded bg-muted px-1 py-0.5 text-xs">agentfit.db</code> first.
159
+ </div>
160
+ </div>
161
+ <DialogFooter className="gap-3">
162
+ <Button variant="outline" onClick={() => setResetOpen(false)}>
163
+ Cancel
164
+ </Button>
165
+ <Button
166
+ variant="destructive"
167
+ disabled={resetting}
168
+ onClick={async () => {
169
+ await handleReset()
170
+ setResetOpen(false)
171
+ }}
172
+ >
173
+ I understand, reset everything
174
+ </Button>
175
+ </DialogFooter>
176
+ </DialogContent>
177
+ </Dialog>
178
+ </div>
179
+ )
180
+ }
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { useData } from '@/components/data-provider'
4
+ import { ToolFlowGraph } from '@/components/tool-flow-graph'
5
+ import { SessionTimeline } from '@/components/session-timeline'
6
+
7
+ export default function FlowPage() {
8
+ const { data } = useData()
9
+ if (!data) return null
10
+
11
+ return (
12
+ <>
13
+ <SessionTimeline sessions={data.sessions} />
14
+ <ToolFlowGraph data={data} />
15
+ </>
16
+ )
17
+ }
@@ -2,11 +2,13 @@
2
2
 
3
3
  import { DataProvider } from '@/components/data-provider'
4
4
  import { DashboardShell } from '@/components/dashboard-shell'
5
+ import { Toaster } from '@/components/ui/sonner'
5
6
 
6
7
  export default function DashboardLayout({ children }: { children: React.ReactNode }) {
7
8
  return (
8
9
  <DataProvider>
9
10
  <DashboardShell>{children}</DashboardShell>
11
+ <Toaster />
10
12
  </DataProvider>
11
13
  )
12
14
  }
@@ -3,21 +3,40 @@
3
3
  import { useData } from '@/components/data-provider'
4
4
  import { FitnessScore } from '@/components/fitness-score'
5
5
  import { OverviewCards } from '@/components/overview-cards'
6
- import { DailyChart } from '@/components/daily-chart'
7
- import { ToolUsageChart } from '@/components/tool-usage-chart'
6
+ import { DailyCostChart, TopCommandsChart, TokenUsageHeatmap, UserVsAssistantChart, InterruptionRateChart, ToolMixChart } from '@/components/daily-chart'
7
+ import { RefreshCw } from 'lucide-react'
8
8
 
9
9
  export default function DashboardPage() {
10
- const { data } = useData()
10
+ const { data, newSessionsAvailable, handleSync, syncing } = useData()
11
11
  if (!data) return null
12
12
 
13
13
  return (
14
14
  <>
15
+ {newSessionsAvailable > 0 && (
16
+ <div className="flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm dark:border-blue-800 dark:bg-blue-950">
17
+ <span>
18
+ <strong>{newSessionsAvailable}</strong> new session{newSessionsAvailable !== 1 ? 's' : ''} detected on disk. Sync to update your dashboard.
19
+ </span>
20
+ <button
21
+ onClick={handleSync}
22
+ disabled={syncing}
23
+ className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50"
24
+ >
25
+ <RefreshCw className={`h-3 w-3 ${syncing ? 'animate-spin' : ''}`} />
26
+ {syncing ? 'Syncing…' : 'Sync now'}
27
+ </button>
28
+ </div>
29
+ )}
15
30
  <FitnessScore data={data} />
16
31
  <OverviewCards overview={data.overview} sessions={data.sessions} />
17
32
  <div className="grid gap-4 lg:grid-cols-3">
18
- <DailyChart daily={data.daily} />
33
+ <DailyCostChart daily={data.daily} />
34
+ <TopCommandsChart />
35
+ <TokenUsageHeatmap daily={data.daily} />
36
+ <UserVsAssistantChart daily={data.daily} />
37
+ <InterruptionRateChart daily={data.daily} />
38
+ <ToolMixChart daily={data.daily} />
19
39
  </div>
20
- <ToolUsageChart toolUsage={data.toolUsage} />
21
40
  </>
22
41
  )
23
42
  }
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { ArrowLeft, Loader2 } from 'lucide-react'
7
+ import { ReportView } from '@/components/report-view'
8
+ import type { ReportContent } from '@/lib/report'
9
+
10
+ interface ReportData {
11
+ id: string
12
+ title: string
13
+ generatedAt: string
14
+ contentJson: ReportContent
15
+ sessionCount: number
16
+ }
17
+
18
+ export default function ReportDetailPage() {
19
+ const params = useParams()
20
+ const id = params.id as string
21
+ const [report, setReport] = useState<ReportData | null>(null)
22
+ const [loading, setLoading] = useState(true)
23
+ const [error, setError] = useState<string | null>(null)
24
+
25
+ useEffect(() => {
26
+ async function load() {
27
+ try {
28
+ const res = await fetch(`/api/reports/${id}`)
29
+ if (!res.ok) throw new Error('Report not found')
30
+ setReport(await res.json())
31
+ } catch (e) {
32
+ setError((e as Error).message)
33
+ } finally {
34
+ setLoading(false)
35
+ }
36
+ }
37
+ load()
38
+ }, [id])
39
+
40
+ if (loading) {
41
+ return (
42
+ <div className="flex items-center gap-2 py-8 text-muted-foreground">
43
+ <Loader2 className="h-4 w-4 animate-spin" /> Loading report...
44
+ </div>
45
+ )
46
+ }
47
+
48
+ if (error || !report) {
49
+ return (
50
+ <div className="space-y-4">
51
+ <Link href="/reports" className="flex items-center gap-1 text-sm text-primary hover:underline">
52
+ <ArrowLeft className="h-4 w-4" /> Back to reports
53
+ </Link>
54
+ <div className="text-destructive">{error || 'Report not found'}</div>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ return (
60
+ <div className="space-y-4">
61
+ <div className="flex items-center justify-between">
62
+ <Link href="/reports" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
63
+ <ArrowLeft className="h-4 w-4" /> Back to reports
64
+ </Link>
65
+ <span className="text-xs text-muted-foreground">
66
+ Generated {new Date(report.generatedAt).toLocaleString()} · {report.sessionCount} sessions
67
+ </span>
68
+ </div>
69
+ <ReportView content={report.contentJson} />
70
+ </div>
71
+ )
72
+ }