Skip to content

Commit a6f6bb1

Browse files
committed
feat(platform-notifications): PR Fixes
1 parent 1980052 commit a6f6bb1

File tree

5 files changed

+73
-10
lines changed

5 files changed

+73
-10
lines changed

apps/webapp/app/components/navigation/NotificationPanel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,10 @@ function getMarkdownComponents(onLinkClick: () => void) {
280280
target="_blank"
281281
rel="noopener noreferrer"
282282
className="text-indigo-400 underline hover:text-indigo-300 transition-colors"
283-
onClick={onLinkClick}
283+
onClick={(e) => {
284+
e.stopPropagation();
285+
onLinkClick();
286+
}}
284287
>
285288
{children}
286289
</a>

apps/webapp/app/routes/admin.api.v1.platform-notifications.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ export async function action({ request }: ActionFunctionArgs) {
4242
return json({ error: message }, { status });
4343
}
4444

45-
const body = await request.json();
45+
let body: unknown;
46+
try {
47+
body = await request.json();
48+
} catch {
49+
return json({ error: "Invalid JSON body" }, { status: 400 });
50+
}
4651
const result = await createPlatformNotification(body as CreatePlatformNotificationInput);
4752

4853
if (result.isErr()) {

apps/webapp/app/routes/resources.platform-notifications.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ const POLL_INTERVAL_MS = 60000; // 1 minute
3434

3535
export function usePlatformNotifications(organizationId: string, projectId: string) {
3636
const fetcher = useFetcher<typeof loader>();
37-
const hasInitiallyFetched = useRef(false);
37+
const lastLoadedUrl = useRef<string | null>(null);
3838

3939
useEffect(() => {
4040
const url = `/resources/platform-notifications?organizationId=${encodeURIComponent(organizationId)}&projectId=${encodeURIComponent(projectId)}`;
4141

42-
if (!hasInitiallyFetched.current && fetcher.state === "idle") {
43-
hasInitiallyFetched.current = true;
42+
if (lastLoadedUrl.current !== url && fetcher.state === "idle") {
43+
lastLoadedUrl.current = url;
4444
fetcher.load(url);
4545
}
4646

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Redis } from "ioredis";
2+
import { env } from "~/env.server";
3+
import { singleton } from "~/utils/singleton";
4+
import { logger } from "./logger.server";
5+
6+
const KEY_PREFIX = "cli-notif-ctr:";
7+
const MAX_COUNTER = 1000;
8+
9+
function initializeRedis(): Redis | undefined {
10+
const host = env.CACHE_REDIS_HOST;
11+
if (!host) return undefined;
12+
13+
return new Redis({
14+
connectionName: "platformNotificationCounter",
15+
host,
16+
port: env.CACHE_REDIS_PORT,
17+
username: env.CACHE_REDIS_USERNAME,
18+
password: env.CACHE_REDIS_PASSWORD,
19+
keyPrefix: "tr:",
20+
enableAutoPipelining: true,
21+
...(env.CACHE_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
22+
});
23+
}
24+
25+
const redis = singleton("platformNotificationCounter", initializeRedis);
26+
27+
/** Increment and return the user's CLI request counter (1-based, wraps at 1001→1). */
28+
export async function incrementCliRequestCounter(userId: string): Promise<number> {
29+
if (!redis) return 1;
30+
31+
try {
32+
const key = `${KEY_PREFIX}${userId}`;
33+
const value = await redis.incr(key);
34+
35+
if (value > MAX_COUNTER) {
36+
await redis.set(key, "1");
37+
return 1;
38+
}
39+
40+
return value;
41+
} catch (error) {
42+
logger.error("Failed to increment CLI notification counter", { userId, error });
43+
return 1;
44+
}
45+
}

apps/webapp/app/services/platformNotifications.server.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod";
22
import { errAsync, fromPromise, type ResultAsync } from "neverthrow";
33
import { prisma } from "~/db.server";
44
import { type PlatformNotificationScope, type PlatformNotificationSurface } from "@trigger.dev/database";
5+
import { incrementCliRequestCounter } from "./platformNotificationCounter.server";
56

67
// --- Payload schema (spec v1) ---
78

@@ -275,6 +276,9 @@ export async function recordNotificationClicked({
275276
// --- Read: recent changelogs (for Help & Feedback) ---
276277

277278
export async function getRecentChangelogs({ limit = 2 }: { limit?: number } = {}) {
279+
// NOTE: Intentionally not filtering by archivedAt, startsAt, or endsAt.
280+
// We want to show archived and expired changelogs in the "What's new" section
281+
// so users can still find recent release notes.
278282
const notifications = await prisma.platformNotification.findMany({
279283
where: {
280284
surface: "WEBAPP",
@@ -398,6 +402,11 @@ export async function getNextCliNotification({
398402

399403
const sorted = [...notifications].sort(compareNotifications);
400404

405+
// Global per-user request counter stored in Redis, used for cliShowEvery modulo.
406+
// This is independent of per-notification showCount so that cliMaxShowCount
407+
// correctly tracks actual displays, not API encounters.
408+
const requestCounter = await incrementCliRequestCounter(userId);
409+
401410
for (const n of sorted) {
402411
const interaction = n.interactions[0] ?? null;
403412

@@ -407,6 +416,12 @@ export async function getNextCliNotification({
407416
const parsed = PayloadV1Schema.safeParse(n.payload);
408417
if (!parsed.success) continue;
409418

419+
// Check cliShowEvery using the global request counter
420+
if (n.cliShowEvery !== null && requestCounter % n.cliShowEvery !== 0) {
421+
continue;
422+
}
423+
424+
// Only increment showCount when the notification will actually be displayed
410425
const updated = await prisma.platformNotificationInteraction.upsert({
411426
where: { notificationId_userId: { notificationId: n.id, userId } },
412427
update: { showCount: { increment: 1 } },
@@ -418,11 +433,6 @@ export async function getNextCliNotification({
418433
},
419434
});
420435

421-
// If cliShowEvery is set, only return on every N-th request
422-
if (n.cliShowEvery !== null && updated.showCount % n.cliShowEvery !== 0) {
423-
continue;
424-
}
425-
426436
return {
427437
id: n.id,
428438
payload: parsed.data,

0 commit comments

Comments
 (0)