Skip to content

Commit 4f553d0

Browse files
ofershapcursoragent
andcommitted
fix: deduplicate daily_spend rows and make dashboard search filter all cards
daily_spend table has composite PK (date, email, cycle_start), causing duplicate rows per (date, email) when users span multiple billing cycles. All queries now use MAX(spend_cents) GROUP BY date, email to deduplicate before aggregation — fixes inflated totals and duplicate chart X-axis labels. Dashboard search now filters KPI strip, spend trend chart, and daily spend breakdown chart in addition to the bar chart and members table. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5712284 commit 4f553d0

2 files changed

Lines changed: 101 additions & 37 deletions

File tree

src/app/dashboard-client.tsx

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
126126
}
127127

128128
const timeLabel = formatTimeLabel(days);
129+
const isSearching = search.trim().length > 0;
129130
const totalLines = stats.dailyTeamActivity.reduce((s, d) => s + d.total_lines_added, 0);
130131
const effectiveDays = Math.min(days, stats.cycleDays);
131132
const cycleStartDate = new Date(stats.cycleStart);
@@ -141,9 +142,44 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
141142
);
142143
const daysLeft = Math.max(0, Math.ceil((cycleEndDate.getTime() - Date.now()) / 86_400_000));
143144

145+
const filteredEmails = useMemo(() => new Set(filteredUsers.map((u) => u.email)), [filteredUsers]);
146+
147+
const filteredSpendCents = useMemo(
148+
() =>
149+
isSearching ? filteredUsers.reduce((s, u) => s + u.spend_cents, 0) : stats.totalSpendCents,
150+
[isSearching, filteredUsers, stats.totalSpendCents],
151+
);
152+
const filteredAgentRequests = useMemo(
153+
() =>
154+
isSearching
155+
? filteredUsers.reduce((s, u) => s + u.agent_requests, 0)
156+
: stats.totalAgentRequests,
157+
[isSearching, filteredUsers, stats.totalAgentRequests],
158+
);
159+
const filteredLinesAdded = useMemo(
160+
() => (isSearching ? filteredUsers.reduce((s, u) => s + u.lines_added, 0) : totalLines),
161+
[isSearching, filteredUsers, totalLines],
162+
);
163+
164+
const filteredBreakdown = useMemo(() => {
165+
if (!isSearching) return data.dailySpendBreakdown;
166+
return data.dailySpendBreakdown.filter((r) => filteredEmails.has(r.email));
167+
}, [isSearching, data.dailySpendBreakdown, filteredEmails]);
168+
169+
const filteredTeamDailySpend = useMemo(() => {
170+
if (!isSearching) return data.teamDailySpend;
171+
const byDate = new Map<string, number>();
172+
for (const r of filteredBreakdown) {
173+
byDate.set(r.date, (byDate.get(r.date) ?? 0) + r.spend_cents);
174+
}
175+
return [...byDate.entries()]
176+
.sort(([a], [b]) => a.localeCompare(b))
177+
.map(([date, spend_cents]) => ({ date, spend_cents }));
178+
}, [isSearching, data.teamDailySpend, filteredBreakdown]);
179+
144180
const dailySpendData = useMemo(() => {
145-
return buildDailySpendData(data.dailySpendBreakdown);
146-
}, [data.dailySpendBreakdown]);
181+
return buildDailySpendData(filteredBreakdown);
182+
}, [filteredBreakdown]);
147183

148184
return (
149185
<div className="space-y-3">
@@ -170,15 +206,18 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
170206
))}
171207
</div>
172208
{loading && <span className="text-[11px] text-zinc-500 animate-pulse">Updating...</span>}
173-
<div className="ml-auto text-[11px] text-zinc-600">{stats.totalMembers} members</div>
209+
<div className="ml-auto text-[11px] text-zinc-600">
210+
{isSearching ? `${filteredUsers.length} / ` : ""}
211+
{stats.totalMembers} members
212+
</div>
174213
</div>
175214

176215
{/* ── KPI Strip ── */}
177216
<div className="flex items-stretch gap-2 overflow-x-auto">
178217
<Kpi
179-
label={`Team Spend (${timeLabel})`}
180-
value={`$${Math.round(stats.totalSpendCents / 100).toLocaleString()}`}
181-
sub={`~$${Math.round(stats.totalSpendCents / 100 / (effectiveDays || 1))}/day`}
218+
label={`${isSearching ? "Filtered" : "Team"} Spend (${timeLabel})`}
219+
value={`$${Math.round(filteredSpendCents / 100).toLocaleString()}`}
220+
sub={`~$${Math.round(filteredSpendCents / 100 / (effectiveDays || 1))}/day`}
182221
/>
183222
<Kpi
184223
label="Billing Cycle"
@@ -199,18 +238,22 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
199238
<KpiSep />
200239
<Kpi
201240
label={`Active (${timeLabel})`}
202-
value={stats.activeMembers.toString()}
203-
sub={`${Math.round((stats.activeMembers / stats.totalMembers) * 100)}% of team`}
241+
value={isSearching ? filteredUsers.length.toString() : stats.activeMembers.toString()}
242+
sub={
243+
isSearching
244+
? `${filteredUsers.length} of ${stats.totalMembers} members`
245+
: `${Math.round((stats.activeMembers / stats.totalMembers) * 100)}% of team`
246+
}
204247
/>
205248
<Kpi
206249
label={`Requests (${timeLabel})`}
207-
value={fmt(stats.totalAgentRequests)}
208-
sub={`~${fmt(Math.round(stats.totalAgentRequests / (effectiveDays || 1)))}/day`}
250+
value={fmt(filteredAgentRequests)}
251+
sub={`~${fmt(Math.round(filteredAgentRequests / (effectiveDays || 1)))}/day`}
209252
/>
210253
<Kpi
211254
label={`Lines (${timeLabel})`}
212-
value={fmt(totalLines)}
213-
sub={`~${fmt(Math.round(totalLines / (effectiveDays || 1)))}/day`}
255+
value={fmt(filteredLinesAdded)}
256+
sub={`~${fmt(Math.round(filteredLinesAdded / (effectiveDays || 1)))}/day`}
214257
/>
215258
</div>
216259

@@ -234,7 +277,7 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
234277

235278
{/* ── Row: Team Spend Trend + Model Cost Breakdown ── */}
236279
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
237-
<SpendTrendChart data={data.teamDailySpend} selectedDays={days} />
280+
<SpendTrendChart data={filteredTeamDailySpend} selectedDays={days} />
238281
<ModelCostTable data={data.modelCosts} />
239282
</div>
240283

src/lib/db.ts

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,9 @@ export function upsertBillingGroups(
496496
export function getUserDailySpend(email: string): Array<{ date: string; spend_cents: number }> {
497497
const db = getDb();
498498
return db
499-
.prepare("SELECT date, spend_cents FROM daily_spend WHERE email = ? ORDER BY date")
499+
.prepare(
500+
"SELECT date, MAX(spend_cents) as spend_cents FROM daily_spend WHERE email = ? GROUP BY date ORDER BY date",
501+
)
500502
.all(email) as Array<{ date: string; spend_cents: number }>;
501503
}
502504

@@ -800,7 +802,10 @@ export function getFullDashboard(days: number = 7): FullDashboard {
800802

801803
const spendRow = db
802804
.prepare(
803-
"SELECT COALESCE(SUM(spend_cents), 0) as total FROM daily_spend WHERE date >= date('now', ?)",
805+
`SELECT COALESCE(SUM(spend), 0) as total FROM (
806+
SELECT date, email, MAX(spend_cents) as spend FROM daily_spend
807+
WHERE date >= date('now', ?) GROUP BY date, email
808+
)`,
804809
)
805810
.get(dateFilter) as { total: number };
806811

@@ -836,9 +841,14 @@ export function getFullDashboard(days: number = 7): FullDashboard {
836841

837842
const rankedUsers = db
838843
.prepare(
839-
`WITH user_spend AS (
840-
SELECT email, SUM(spend_cents) as spend_cents
844+
`WITH deduped_spend AS (
845+
SELECT date, email, MAX(spend_cents) as spend_cents
841846
FROM daily_spend WHERE date >= date('now', ?)
847+
GROUP BY date, email
848+
),
849+
user_spend AS (
850+
SELECT email, SUM(spend_cents) as spend_cents
851+
FROM deduped_spend
842852
GROUP BY email
843853
),
844854
activity AS (
@@ -884,9 +894,10 @@ export function getFullDashboard(days: number = 7): FullDashboard {
884894
const teamDailySpend = db
885895
.prepare(
886896
`
887-
SELECT date, SUM(spend_cents) as spend_cents
888-
FROM daily_spend WHERE date >= date('now', ?)
889-
GROUP BY date ORDER BY date
897+
SELECT date, SUM(spend) as spend_cents FROM (
898+
SELECT date, email, MAX(spend_cents) as spend FROM daily_spend
899+
WHERE date >= date('now', ?) GROUP BY date, email
900+
) GROUP BY date ORDER BY date
890901
`,
891902
)
892903
.all(chartDateFilter) as Array<{ date: string; spend_cents: number }>;
@@ -897,9 +908,14 @@ export function getFullDashboard(days: number = 7): FullDashboard {
897908
SELECT ds.date, ds.email,
898909
COALESCE(m.name, ds.email) as name,
899910
ds.spend_cents
900-
FROM daily_spend ds
911+
FROM (
912+
SELECT date, email, MAX(spend_cents) as spend_cents
913+
FROM daily_spend
914+
WHERE date >= date('now', ?)
915+
GROUP BY date, email
916+
) ds
901917
LEFT JOIN members m ON ds.email = m.email
902-
WHERE ds.spend_cents > 0 AND ds.date >= date('now', ?)
918+
WHERE ds.spend_cents > 0
903919
ORDER BY ds.date, ds.spend_cents DESC
904920
`,
905921
)
@@ -922,9 +938,14 @@ export function getFullDashboard(days: number = 7): FullDashboard {
922938
SELECT email, most_used_model FROM user_model
923939
WHERE (email, reqs) IN (SELECT email, MAX(reqs) FROM user_model GROUP BY email)
924940
),
941+
deduped AS (
942+
SELECT date, email, MAX(spend_cents) as spend_cents
943+
FROM daily_spend WHERE date >= date('now', ?)
944+
GROUP BY date, email
945+
),
925946
user_spend AS (
926947
SELECT email, SUM(spend_cents) as spend_cents
927-
FROM daily_spend WHERE date >= date('now', ?)
948+
FROM deduped
928949
GROUP BY email
929950
)
930951
SELECT pm.most_used_model as model, COUNT(*) as users,
@@ -1118,9 +1139,9 @@ export function getTeamDailySpend(): Array<{ date: string; spend_cents: number }
11181139
return db
11191140
.prepare(
11201141
`
1121-
SELECT date, SUM(spend_cents) as spend_cents
1122-
FROM daily_spend
1123-
GROUP BY date ORDER BY date
1142+
SELECT date, SUM(spend) as spend_cents FROM (
1143+
SELECT date, email, MAX(spend_cents) as spend FROM daily_spend GROUP BY date, email
1144+
) GROUP BY date ORDER BY date
11241145
`,
11251146
)
11261147
.all() as Array<{ date: string; spend_cents: number }>;
@@ -1139,7 +1160,10 @@ export function getDailySpendBreakdown(): Array<{
11391160
SELECT ds.date, ds.email,
11401161
COALESCE(m.name, ds.email) as name,
11411162
ds.spend_cents
1142-
FROM daily_spend ds
1163+
FROM (
1164+
SELECT date, email, MAX(spend_cents) as spend_cents
1165+
FROM daily_spend GROUP BY date, email
1166+
) ds
11431167
LEFT JOIN members m ON ds.email = m.email
11441168
WHERE ds.spend_cents > 0
11451169
ORDER BY ds.date, ds.spend_cents DESC
@@ -1281,9 +1305,7 @@ export function upsertAnalyticsClientVersions(entries: AnalyticsClientVersionsEn
12811305
tx();
12821306
}
12831307

1284-
export function getAnalyticsDAU(
1285-
days: number = 30,
1286-
): Array<{
1308+
export function getAnalyticsDAU(days: number = 30): Array<{
12871309
date: string;
12881310
dau: number;
12891311
cli_dau: number;
@@ -1320,9 +1342,7 @@ export function getAnalyticsModelUsageTrend(
13201342
}>;
13211343
}
13221344

1323-
export function getAnalyticsModelUsageSummary(
1324-
days: number = 30,
1325-
): Array<{
1345+
export function getAnalyticsModelUsageSummary(days: number = 30): Array<{
13261346
model: string;
13271347
total_messages: number;
13281348
total_users: number;
@@ -1346,9 +1366,7 @@ export function getAnalyticsModelUsageSummary(
13461366
}>;
13471367
}
13481368

1349-
export function getAnalyticsAgentEditsTrend(
1350-
days: number = 30,
1351-
): Array<{
1369+
export function getAnalyticsAgentEditsTrend(days: number = 30): Array<{
13521370
date: string;
13531371
accepted_diffs: number;
13541372
rejected_diffs: number;
@@ -1476,7 +1494,10 @@ export function getModelEfficiency(): ModelEfficiency[] {
14761494
THEN ROUND(SUM(ds.spend_cents) / 100.0 / SUM(du.accepted_lines_added), 4)
14771495
ELSE 0 END as cost_per_useful_line
14781496
FROM daily_usage du
1479-
JOIN daily_spend ds ON du.email = ds.email AND du.date = ds.date
1497+
JOIN (
1498+
SELECT date, email, MAX(spend_cents) as spend_cents
1499+
FROM daily_spend GROUP BY date, email
1500+
) ds ON du.email = ds.email AND du.date = ds.date
14801501
WHERE du.is_active = 1
14811502
AND du.most_used_model != ''
14821503
AND du.agent_requests > 0

0 commit comments

Comments
 (0)