Skip to content

Commit 600a906

Browse files
committed
feat: enhance caching and diagnostics features
- Implemented in-flight request handling in client cache to prevent duplicate fetches during rapid remounts. - Added a function to clear cache entries by key in the client cache. - Increased database connection pool size to improve performance during high query loads. - Introduced a method to clear cached entries by prefix in server cache for better cache management. - Refactored campaign list retrieval to improve performance and maintainability, including backfilling missing share slugs. - Created new components for account URL filtering, campaign detail display, and diagnostics refresh functionality. - Developed a shared diagnostics data retrieval function to streamline data access for the diagnostics page. - Added server-side statistics aggregation for dashboard metrics, including daily DM counts and keyword normalization.
1 parent efab86e commit 600a906

22 files changed

Lines changed: 1315 additions & 1054 deletions

File tree

app/(dashboard)/campaigns/[id]/page.tsx

Lines changed: 23 additions & 435 deletions
Large diffs are not rendered by default.

app/(dashboard)/dashboard/page.tsx

Lines changed: 49 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,81 @@
1-
"use client";
1+
import Link from "next/link";
2+
import { redirect } from "next/navigation";
3+
import AccountUrlFilter from "@/components/account-url-filter";
4+
import StatCard from "@/components/stat-card";
5+
import StatusBadge from "@/components/status-badge";
6+
import { getDashboardStats } from "@/lib/server/stats";
7+
import { getCurrentWorkspaceContext } from "@/lib/workspace-access";
28

39
/**
4-
* Dashboard Home Page
10+
* Dashboard Home Page (Server Component)
511
*
6-
* Overview cards, 7-day chart, and recent activity feed.
12+
* Renders the stats tiles, 7-day chart, keywords, and recent activity directly
13+
* from the database — no client fetch, no JSON round-trip. The account filter
14+
* lives in the URL (`?instagramAccountId=`), so switching accounts is a plain
15+
* navigation that re-renders this component; the only client island is the
16+
* account `<select>`.
717
*/
818

9-
import { useState } from "react";
10-
import AccountSelect, { type AccountOption } from "@/components/account-select";
11-
import StatCard from "@/components/stat-card";
12-
import StatusBadge from "@/components/status-badge";
13-
import { useCachedFetch } from "@/lib/client-cache";
14-
15-
interface DashboardStats {
16-
userName: string | null;
17-
contactsCount: number;
18-
totalAutomations: number;
19-
activeAutomations: number;
20-
dmsSentToday: number;
21-
dmsSentWeek: number;
22-
dmsSentMonth: number;
23-
dmsSkippedMonth: number;
24-
dmsFailedMonth: number;
25-
totalDMs: number;
26-
clicksThisMonth: number;
27-
totalClicks: number;
28-
ctrThisMonth: number;
29-
instagramAccounts: AccountOption[];
30-
selectedInstagramAccountId: string | null;
31-
topKeywords: { keyword: string; count: number }[];
32-
dailyDMs: { date: string; count: number }[];
33-
recentLogs: Array<{
34-
id: string;
35-
commenterName: string | null;
36-
commentText: string;
37-
status: string;
38-
createdAt: string;
39-
automation: { name: string };
40-
instagramAccount?: { username: string };
41-
}>;
42-
}
19+
export const dynamic = "force-dynamic";
4320

44-
export default function DashboardPage() {
45-
const [selectedAccountId, setSelectedAccountId] = useState("all");
21+
export default async function DashboardPage(props: PageProps<"/dashboard">) {
22+
// One auth() session lookup + one membership query covers both the
23+
// workspace and the user — the previous code did two separate auth()
24+
// round trips (getCurrentWorkspaceId + getCurrentUserId).
25+
const context = await getCurrentWorkspaceContext();
26+
if (!context) redirect("/login");
4627

47-
// Cached copy paints instantly on return visits; a background fetch keeps it
48-
// fresh. 30s max age is plenty for dashboard tiles.
49-
const statsFetch = useCachedFetch<DashboardStats>(
50-
`dash:stats:${selectedAccountId}`,
51-
async () => {
52-
const params = new URLSearchParams();
53-
if (selectedAccountId !== "all") {
54-
params.set("instagramAccountId", selectedAccountId);
55-
}
28+
const searchParams = await props.searchParams;
29+
const selectedAccountId =
30+
typeof searchParams.instagramAccountId === "string"
31+
? searchParams.instagramAccountId
32+
: "all";
5633

57-
const res = await fetch(
58-
`/api/dashboard/stats${params.size ? `?${params}` : ""}`
59-
);
60-
const data = await res.json();
61-
if (!data.success) {
62-
throw new Error(data.error ?? "Failed to load stats");
63-
}
64-
return data.data as DashboardStats;
65-
},
66-
{ maxAgeMs: 30_000 }
34+
const stats = await getDashboardStats(
35+
context.workspaceId,
36+
context.userId,
37+
selectedAccountId === "all" ? null : selectedAccountId
6738
);
68-
const stats = statsFetch.data;
69-
70-
function handleAccountChange(accountId: string) {
71-
setSelectedAccountId(accountId);
72-
}
73-
74-
if (statsFetch.loading && !stats) {
75-
return (
76-
<div className="space-y-6">
77-
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 sm:gap-4">
78-
{[...Array(6)].map((_, i) => (
79-
<div key={i} className="panel rounded p-5 h-32">
80-
<div className="w-10 h-10 rounded bg-surface-hover" />
81-
<div className="mt-4 h-6 w-16 bg-surface-hover rounded" />
82-
<div className="mt-2 h-4 w-24 bg-surface-hover/60 rounded" />
83-
</div>
84-
))}
85-
</div>
86-
</div>
87-
);
88-
}
89-
90-
const maxDM = Math.max(...(stats?.dailyDMs.map((d) => d.count) ?? [1]), 1);
9139

92-
const connectedCount = stats?.instagramAccounts.length ?? 0;
40+
const maxDM = Math.max(...stats.dailyDMs.map((d) => d.count), 1);
41+
const connectedCount = stats.instagramAccounts.length;
9342

9443
return (
9544
<div className="space-y-8">
9645
{/* Greeting header */}
9746
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
9847
<div>
9948
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
100-
Hello, {stats?.userName ?? "there"}!
49+
Hello, {stats.userName ?? "there"}!
10150
</h1>
10251
<p className="mt-1 text-sm text-muted">
10352
{connectedCount} connected{" "}
10453
{connectedCount === 1 ? "account" : "accounts"}
10554
{" · "}
106-
{stats?.contactsCount ?? 0}{" "}
107-
{stats?.contactsCount === 1 ? "contact" : "contacts"}
55+
{stats.contactsCount}{" "}
56+
{stats.contactsCount === 1 ? "contact" : "contacts"}
10857
{" · "}
109-
<a href="/logs" className="text-accent hover:underline">
58+
<Link href="/logs" className="text-accent hover:underline">
11059
See activity
111-
</a>
60+
</Link>
11261
</p>
11362
</div>
114-
{stats && stats.instagramAccounts.length > 1 && (
115-
<AccountSelect
63+
{stats.instagramAccounts.length > 1 && (
64+
<AccountUrlFilter
11665
accounts={stats.instagramAccounts}
11766
value={selectedAccountId}
118-
onChange={handleAccountChange}
11967
/>
12068
)}
12169
</div>
12270

12371
{/* Stat Cards */}
12472
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 sm:gap-4">
125-
<StatCard
126-
label="Active Campaigns"
127-
value={stats?.activeAutomations ?? 0}
128-
/>
129-
<StatCard label="DMs Sent" value={stats?.dmsSentMonth ?? 0} />
130-
<StatCard label="Skipped" value={stats?.dmsSkippedMonth ?? 0} />
131-
<StatCard label="Failed" value={stats?.dmsFailedMonth ?? 0} />
132-
<StatCard label="Clicks" value={stats?.clicksThisMonth ?? 0} />
133-
<StatCard label="CTR" value={`${stats?.ctrThisMonth ?? 0}%`} />
73+
<StatCard label="Active Campaigns" value={stats.activeAutomations} />
74+
<StatCard label="DMs Sent" value={stats.dmsSentMonth} />
75+
<StatCard label="Skipped" value={stats.dmsSkippedMonth} />
76+
<StatCard label="Failed" value={stats.dmsFailedMonth} />
77+
<StatCard label="Clicks" value={stats.clicksThisMonth} />
78+
<StatCard label="CTR" value={`${stats.ctrThisMonth}%`} />
13479
</div>
13580

13681
{/* Chart + Recent Activity */}
@@ -139,7 +84,7 @@ export default function DashboardPage() {
13984
<div className="lg:col-span-3 panel rounded p-4 sm:p-6">
14085
<h2 className="text-sm font-semibold text-foreground mb-6">DMs — Last 7 Days</h2>
14186
<div className="flex items-end gap-1.5 h-40 sm:gap-2">
142-
{stats?.dailyDMs.map((day) => (
87+
{stats.dailyDMs.map((day) => (
14388
<div key={day.date} className="min-w-0 flex-1 flex flex-col items-center gap-2">
14489
<span className="text-xs text-muted font-medium">{day.count}</span>
14590
<div
@@ -159,10 +104,10 @@ export default function DashboardPage() {
159104
<div className="lg:col-span-1 panel rounded p-4 sm:p-6">
160105
<h2 className="text-sm font-semibold text-foreground mb-4">Top Keywords</h2>
161106
<div className="space-y-3">
162-
{stats?.topKeywords.length === 0 && (
107+
{stats.topKeywords.length === 0 && (
163108
<p className="text-sm text-muted py-8">No keyword matches yet</p>
164109
)}
165-
{stats?.topKeywords.map((keyword) => (
110+
{stats.topKeywords.map((keyword) => (
166111
<div key={keyword.keyword} className="flex items-center justify-between gap-3">
167112
<span className="truncate text-sm font-medium text-foreground">
168113
{keyword.keyword}
@@ -177,10 +122,10 @@ export default function DashboardPage() {
177122
<div className="lg:col-span-2 panel rounded p-4 sm:p-6">
178123
<h2 className="text-sm font-semibold text-foreground mb-4">Recent Activity</h2>
179124
<div className="space-y-3 max-h-60 overflow-y-auto">
180-
{stats?.recentLogs.length === 0 && (
125+
{stats.recentLogs.length === 0 && (
181126
<p className="text-sm text-muted text-center py-8">No activity yet</p>
182127
)}
183-
{stats?.recentLogs.map((log) => (
128+
{stats.recentLogs.map((log) => (
184129
<div
185130
key={log.id}
186131
className="flex items-center justify-between gap-3 py-2 border-b border-border last:border-0"

0 commit comments

Comments
 (0)