Skip to content

Commit 6dec7c6

Browse files
committed
simplify stats page UI: remove play button, milestones, and 500k target
1 parent e922e4e commit 6dec7c6

4 files changed

Lines changed: 50 additions & 170 deletions

File tree

changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- Simplified stats page UI
12+
- Removed Play/Reset animation controls from growth chart (static display only)
13+
- Removed milestone progress bar and "% to target" from message counter
14+
- Removed "Target: 500k" from growth chart footer
15+
- Removed "9am PT snapshot" label (refresh button available for manual updates)
16+
- Stats can be refreshed on demand (no rate limit on read-only queries)
17+
918
### Fixed
1019

1120
- Fixed mobile scrolling issues across Dashboard

files.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ React frontend application.
5757
| File | Description |
5858
|------|-------------|
5959
| `Login.tsx` | Public homepage with WorkOS AuthKit integration. Shows "Go to Dashboard" button when logged in (no auto-redirect), "Sign in" when logged out. Includes privacy messaging, "Syncs with" section showing supported CLI tools (OpenCode, Claude Code, Droid, Codex CLI, Cursor with coming soon badge), getting started section with plugin links including codex-sync (mobile-visible), tan mode theme support with footer (theme switcher, Terms/Privacy links), footer icons (GitHub, Discord, Support, Discussions), updated mockup with view tabs and OC/CC source badges (desktop-only), feature list with Sync/Search/Private/Tag/Export/Delete keywords and eval datasets tagline, Watch the demo link, trust message with cloud/local deployment info, real-time Platform Stats leaderboard (Top Models, Top CLI) above Open Source footer link |
60-
| `Stats.tsx` | Public stats page (/stats) with platform statistics isolated from homepage. Includes MessageMilestoneCounter (daily 9am PT snapshot with dynamic milestones, progress bar), AnimatedGrowthChart (SVG chart with play/reset animation showing cumulative message growth over days, dynamic Y-axis scaling, X-axis date labels), plus a local error boundary and data guards so chart failures do not blank the app. Theme toggle, temp refresh button, back link to homepage. No auth required. |
60+
| `Stats.tsx` | Public stats page (/stats) with platform statistics isolated from homepage. Includes MessageMilestoneCounter (total messages synced), GrowthChart (static SVG chart showing cumulative message growth over 60 days, dynamic Y-axis scaling, X-axis date labels), plus a local error boundary and data guards so chart failures do not blank the app. Theme toggle, temp refresh button, back link to homepage. No auth required. |
6161
| `Dashboard.tsx` | Main dashboard with custom themed source filter dropdown (filters by user's enabled agents from Settings, dark/tan mode support, click-outside close, escape key close), source badges (CC/OC/FD), eval toggle button, Context link with search icon, setup banner for new users with 3-column plugin cards (OpenCode, Claude Code, Factory Droid), mobile-optimized header/filters/session rows, URL param support for deep linking from Context search (?session=id), Cmd/Ctrl+K shortcut to open Context search, MessageBubble with content normalization helpers (getPartTextContent, getToolName) for multi-plugin format support, flash-free session transitions using lastValidSessionRef cache (shows cached content until new data loads, subtle corner spinner), and five views: Overview (responsive stat grids), Sessions (mobile-friendly list with stacked layout), Evals (eval-ready sessions with export modal), Analytics (responsive breakdowns with collapsible filters), Wrapped (Daily Sync Wrapped visualization) |
6262
| `Settings.tsx` | Tabbed settings: API Access (two-column layout with Plugin Setup and AI Coding Agents sections, keys, endpoints with plugin links including codex-sync), Profile (collapsible section for privacy, account info, Legal section with Terms/Privacy links, Danger Zone with delete data/account options). AI Coding Agents section lets users enable/disable CLI tools for the source filter dropdown (OpenCode, Claude Code, Factory Droid, Codex CLI with supported status, Cursor, Continue, Amp, Aider, Goose, Mentat, Cline, Kilo Code). Back link navigates to /dashboard |
6363
| `Docs.tsx` | Comprehensive documentation page with instant typeahead search (Cmd/Ctrl+K shortcut), left sidebar navigation (hidden scrollbar) with npm links (opencode-sync-plugin, claude-code-sync, codex-sync), right table of contents, anchor tags, copy/view as markdown buttons, mobile responsive, works with both dark/tan themes. Search indexes all sections with keywords for quick navigation to any topic via hash anchor. Covers use hosted version (with features, 3-plugin install cards with OC/CC/CX badges, login/sync for all three plugins), self-hosting requirements with cloud and 100% local deployment options, quick start (3-plugin cards), dashboard features, OpenCode plugin, Claude Code plugin, Codex CLI plugin with full documentation sections, API reference, search types, authentication, hosting, fork guide, troubleshooting, and FAQ. Links to opencode.ai, claude.ai, and openai.com/codex in hero and plugin sections. |

src/pages/Stats.tsx

Lines changed: 27 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {
88
Moon,
99
MessagesSquare,
1010
Zap,
11-
Play,
12-
RotateCcw,
1311
ArrowLeft,
1412
RefreshCw,
1513
} from "lucide-react";
@@ -25,25 +23,6 @@ function formatNumber(num: number): string {
2523
return num.toString();
2624
}
2725

28-
// TEMP: Calculate next milestone based on current count
29-
function getNextMilestone(count: number): { target: number; previous: number } {
30-
if (count < 500_000) {
31-
return { target: 500_000, previous: 0 };
32-
}
33-
if (count < 1_000_000) {
34-
// 100k increments: 500k -> 600k -> 700k -> etc.
35-
const next = Math.ceil(count / 100_000) * 100_000;
36-
const target = count === next ? next + 100_000 : next;
37-
const prev = target - 100_000;
38-
return { target, previous: prev };
39-
}
40-
// 500k increments for 1M+
41-
const next = Math.ceil(count / 500_000) * 500_000;
42-
const target = count === next ? next + 500_000 : next;
43-
const prev = target - 500_000;
44-
return { target, previous: prev };
45-
}
46-
4726
function getPTOffsetMinutes(date: Date): number {
4827
const parts = new Intl.DateTimeFormat("en-US", {
4928
timeZone: "America/Los_Angeles",
@@ -162,7 +141,7 @@ function useStaticQuery<T>(
162141
return state;
163142
}
164143

165-
// Message milestone counter component
144+
// Message counter component
166145
function MessageMilestoneCounter({
167146
isDark,
168147
refreshToken,
@@ -176,13 +155,7 @@ function MessageMilestoneCounter({
176155
refreshToken,
177156
);
178157

179-
// Calculate milestone targets
180158
const count = messageCount ?? 0;
181-
const { target, previous } = getNextMilestone(count);
182-
183-
// Calculate progress percentage within current milestone range
184-
const range = target - previous;
185-
const progress = range > 0 ? ((count - previous) / range) * 100 : 0;
186159

187160
return (
188161
<div
@@ -201,55 +174,31 @@ function MessageMilestoneCounter({
201174
className={`h-4 w-4 ${isDark ? "text-zinc-500" : "text-[#8b7355]"}`}
202175
/>
203176
Messages Synced
204-
<span
205-
className={`ml-auto text-[10px] font-normal ${isDark ? "text-zinc-600" : "text-[#8b7355]"}`}
206-
>
207-
{loading ? "loading" : "9am PT snapshot"}
208-
</span>
177+
{loading && (
178+
<span
179+
className={`ml-auto text-[10px] font-normal ${isDark ? "text-zinc-600" : "text-[#8b7355]"}`}
180+
>
181+
loading
182+
</span>
183+
)}
209184
</h3>
210185

211186
{/* Count display */}
212-
<div className="mb-3">
187+
<div>
213188
<span
214-
className={`text-2xl font-bold tabular-nums ${
189+
className={`text-3xl font-bold tabular-nums ${
215190
isDark ? "text-zinc-100" : "text-[#1a1a1a]"
216191
}`}
217192
>
218193
{count.toLocaleString()}
219194
</span>
220-
<span
221-
className={`text-sm ml-2 ${isDark ? "text-zinc-500" : "text-[#8b7355]"}`}
222-
>
223-
/ {formatNumber(target)}
224-
</span>
225195
</div>
226-
227-
{/* Progress bar */}
228-
<div
229-
className={`h-2 rounded-full overflow-hidden ${
230-
isDark ? "bg-zinc-800" : "bg-[#e6e4e1]"
231-
}`}
232-
>
233-
<div
234-
className={`h-full rounded-full transition-all duration-500 ${
235-
isDark ? "bg-emerald-500" : "bg-emerald-600"
236-
}`}
237-
style={{ width: `${Math.min(progress, 100)}%` }}
238-
/>
239-
</div>
240-
241-
{/* Percentage */}
242-
<p
243-
className={`text-xs mt-2 ${isDark ? "text-zinc-500" : "text-[#8b7355]"}`}
244-
>
245-
{progress.toFixed(1)}% to {formatNumber(target)}
246-
</p>
247196
</div>
248197
);
249198
}
250199

251-
// Animated growth chart component
252-
function AnimatedGrowthChart({
200+
// Growth chart component
201+
function GrowthChart({
253202
isDark,
254203
refreshToken,
255204
}: {
@@ -259,8 +208,6 @@ function AnimatedGrowthChart({
259208
const { data: growthData } = useStaticQuery<
260209
Array<{ date: string; count: number; cumulative: number }>
261210
>(api.analytics.publicMessageGrowth, {}, refreshToken);
262-
const [isPlaying, setIsPlaying] = useState(false);
263-
const [animationKey, setAnimationKey] = useState(0);
264211

265212
// Get data points - limit to last 60 days for readability
266213
const dataPoints = growthData?.slice(-60) ?? [];
@@ -289,25 +236,6 @@ function AnimatedGrowthChart({
289236

290237
const yAxisMax = getNiceMax(maxCumulative * 1.1);
291238

292-
const handlePlay = () => {
293-
setAnimationKey((prev) => prev + 1);
294-
setIsPlaying(true);
295-
};
296-
297-
const handleReset = () => {
298-
setIsPlaying(false);
299-
setAnimationKey((prev) => prev + 1);
300-
};
301-
302-
// Fallback timeout to stop animation
303-
useEffect(() => {
304-
if (!isPlaying) return;
305-
const timeout = setTimeout(() => {
306-
setIsPlaying(false);
307-
}, 3500);
308-
return () => clearTimeout(timeout);
309-
}, [isPlaying, animationKey]);
310-
311239
// Build SVG path
312240
const chartHeight = 120;
313241
const chartWidth = 100;
@@ -391,47 +319,16 @@ function AnimatedGrowthChart({
391319
: "border-[#e6e4e1] bg-[#f5f3f0]"
392320
}`}
393321
>
394-
<div className="flex items-center justify-between mb-4">
395-
<h3
396-
className={`text-sm font-medium flex items-center gap-2 ${
397-
isDark ? "text-zinc-300" : "text-[#1a1a1a]"
398-
}`}
399-
>
400-
<Zap
401-
className={`h-4 w-4 ${isDark ? "text-zinc-500" : "text-[#8b7355]"}`}
402-
/>
403-
Message Growth
404-
</h3>
405-
406-
<div className="flex items-center gap-2">
407-
{!isPlaying ? (
408-
<button
409-
onClick={handlePlay}
410-
disabled={dataPoints.length === 0}
411-
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
412-
isDark
413-
? "bg-zinc-800 text-zinc-300 hover:bg-zinc-700 disabled:opacity-50"
414-
: "bg-[#ebe9e6] text-[#1a1a1a] hover:bg-[#e6e4e1] disabled:opacity-50"
415-
}`}
416-
>
417-
<Play className="h-3 w-3" />
418-
Play
419-
</button>
420-
) : (
421-
<button
422-
onClick={handleReset}
423-
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
424-
isDark
425-
? "bg-zinc-800 text-zinc-300 hover:bg-zinc-700"
426-
: "bg-[#ebe9e6] text-[#1a1a1a] hover:bg-[#e6e4e1]"
427-
}`}
428-
>
429-
<RotateCcw className="h-3 w-3" />
430-
Reset
431-
</button>
432-
)}
433-
</div>
434-
</div>
322+
<h3
323+
className={`text-sm font-medium mb-4 flex items-center gap-2 ${
324+
isDark ? "text-zinc-300" : "text-[#1a1a1a]"
325+
}`}
326+
>
327+
<Zap
328+
className={`h-4 w-4 ${isDark ? "text-zinc-500" : "text-[#8b7355]"}`}
329+
/>
330+
Message Growth
331+
</h3>
435332

436333
<div className="relative" style={{ height: chartHeight + 20 }}>
437334
{chartPoints.length === 0 ? (
@@ -466,7 +363,6 @@ function AnimatedGrowthChart({
466363
</div>
467364

468365
<svg
469-
key={animationKey}
470366
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
471367
preserveAspectRatio="none"
472368
className="w-full ml-6"
@@ -485,19 +381,6 @@ function AnimatedGrowthChart({
485381
stopOpacity="0"
486382
/>
487383
</linearGradient>
488-
<clipPath id={`clipPath-${animationKey}`}>
489-
<rect x="0" y="0" width={chartWidth} height={chartHeight}>
490-
{isPlaying && (
491-
<animate
492-
attributeName="width"
493-
from="0"
494-
to={chartWidth}
495-
dur="3s"
496-
fill="freeze"
497-
/>
498-
)}
499-
</rect>
500-
</clipPath>
501384
</defs>
502385

503386
<line
@@ -510,26 +393,17 @@ function AnimatedGrowthChart({
510393
strokeDasharray="2,2"
511394
/>
512395

513-
<path
514-
d={areaPath}
515-
fill="url(#growthGradient)"
516-
clipPath={
517-
isPlaying ? `url(#clipPath-${animationKey})` : undefined
518-
}
519-
/>
396+
<path d={areaPath} fill="url(#growthGradient)" />
520397

521398
<path
522399
d={pathData}
523400
fill="none"
524401
stroke={isDark ? "#10b981" : "#059669"}
525402
strokeWidth="1.5"
526403
vectorEffect="non-scaling-stroke"
527-
clipPath={
528-
isPlaying ? `url(#clipPath-${animationKey})` : undefined
529-
}
530404
/>
531405

532-
{chartPoints.length > 0 && !isPlaying && (
406+
{chartPoints.length > 0 && (
533407
<circle
534408
cx={padding.left + innerWidth}
535409
cy={
@@ -561,7 +435,7 @@ function AnimatedGrowthChart({
561435

562436
{chartPoints.length > 0 && (
563437
<div
564-
className={`mt-3 pt-3 border-t flex justify-between text-xs ${
438+
className={`mt-3 pt-3 border-t text-xs ${
565439
isDark
566440
? "border-zinc-800 text-zinc-500"
567441
: "border-[#e6e4e1] text-[#8b7355]"
@@ -573,12 +447,6 @@ function AnimatedGrowthChart({
573447
{chartPoints[chartPoints.length - 1].cumulative.toLocaleString()}
574448
</span>
575449
</span>
576-
<span>
577-
Target:{" "}
578-
<span className={isDark ? "text-zinc-300" : "text-[#1a1a1a]"}>
579-
500k
580-
</span>
581-
</span>
582450
</div>
583451
)}
584452
</div>
@@ -747,7 +615,7 @@ function StatsPageContent({ theme, setTheme }: StatsPageContentProps) {
747615
<MessageMilestoneCounter isDark={isDark} refreshToken={refreshToken} />
748616

749617
{/* Growth chart */}
750-
<AnimatedGrowthChart isDark={isDark} refreshToken={refreshToken} />
618+
<GrowthChart isDark={isDark} refreshToken={refreshToken} />
751619
</div>
752620

753621
{/* Info text */}
@@ -756,7 +624,7 @@ function StatsPageContent({ theme, setTheme }: StatsPageContentProps) {
756624
isDark ? "text-zinc-600" : "text-[#8b7355]"
757625
}`}
758626
>
759-
Stats snapshot from the OpenSync platform. Updates daily at 9am PT.
627+
Stats snapshot from the OpenSync platform.
760628
</p>
761629
</main>
762630
</div>

task.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ OpenSync supports two AI coding tools: **OpenCode** and **Claude Code**.
2424
## Recently Completed (Stats Page)
2525

2626
- [x] Created new public /stats page with platform statistics
27-
- Moved MessageMilestoneCounter and AnimatedGrowthChart from Login page
27+
- Moved MessageMilestoneCounter and GrowthChart from Login page
2828
- Isolated stats components to prevent homepage rendering issues
29-
- Real-time message document count with dynamic milestone targets
30-
- Animated SVG growth chart with play/reset controls
29+
- Total message count display (clean, no milestones)
30+
- Static SVG growth chart showing cumulative growth over 60 days
3131
- Dynamic Y-axis scaling based on actual data
3232
- X-axis date labels (first, middle, last dates)
3333
- Dark/tan theme support with toggle
@@ -36,16 +36,19 @@ OpenSync supports two AI coding tools: **OpenCode** and **Claude Code**.
3636
- [x] Stabilized public stats page rendering
3737
- Added local error boundary to prevent full app blanking
3838
- Guarded growth chart against invalid data points
39-
- [x] Switched stats to daily snapshots at 9am PT
40-
- Loads once from the database (no realtime updates)
41-
- Shows daily update status in the UI
4239
- [x] Fixed 16MB Convex read limit error on stats page
43-
- Changed publicMessageCount to use async iteration instead of collect
44-
- Changed publicMessageGrowth to use async iteration instead of collect
40+
- Rewrote publicMessageCount to sum session.messageCount instead of counting messages
41+
- Rewrote publicMessageGrowth to aggregate from sessions table
4542
- Added temporary refresh button to stats header (to remove later)
43+
- [x] Simplified stats page UI
44+
- Removed Play/Reset animation controls (static chart only)
45+
- Removed milestone progress bar and percentage
46+
- Removed "Target: 500k" from growth chart
47+
- Removed "9am PT snapshot" label
48+
- Stats can be refreshed on demand (read-only queries have no rate limit)
4649
- [x] Added new Convex queries in analytics.ts
47-
- publicMessageCount: returns total message documents (no auth)
48-
- publicMessageGrowth: returns daily counts with cumulative totals (no auth)
50+
- publicMessageCount: sums session messageCount fields (no auth)
51+
- publicMessageGrowth: aggregates daily counts from sessions (no auth)
4952
- Added safety checks for invalid dates in publicMessageGrowth
5053

5154
## Recently Completed (Docs Page codex-sync Updates)

0 commit comments

Comments
 (0)