"use client"; import { useAuth } from "@clerk/nextjs"; import { useCallback, useState } from "react"; function parseNdjsonChunk( buffer: string, onObject: (o: unknown) => void, ): string { const lines = buffer.split("\n"); const rest = lines.pop() ?? ""; for (const row of lines) { const t = row.trim(); if (!t) continue; try { onObject(JSON.parse(t) as unknown); } catch { onObject({ error: `Bad JSON line: ${t.slice(0, 120)}` }); } } return rest; } function isProgress(o: unknown): o is { kind: "progress"; message: string } { return ( typeof o === "object" && o !== null && (o as { kind?: string }).kind === "progress" && typeof (o as { message?: string }).message === "string" ); } export function DigestDebugger() { const { userId: myId } = useAuth(); const [userId, setUserId] = useState(""); const [ignoreRedisHistory, setIgnoreRedisHistory] = useState(false); const [dryRunEmail, setDryRunEmail] = useState(false); const [force, setForce] = useState(false); const [skipCredits, setSkipCredits] = useState(true); const [busy, setBusy] = useState(false); const [logLines, setLogLines] = useState([]); const [lastResult, setLastResult] = useState(null); const append = useCallback((s: string) => { setLogLines((prev) => [...prev, s]); }, []); const runDigest = useCallback(async () => { const uid = userId.trim(); if (!uid) { append("Enter a Clerk userId."); return; } setBusy(true); setLogLines([]); setLastResult(null); append(`POST /api/admin/trigger-digest (userId=${uid.slice(0, 12)}…)`); try { const res = await fetch("/api/admin/trigger-digest", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId: uid, ignoreRedisHistory, dryRunEmail, force, skipCredits, }), }); if (!res.ok || !res.body) { const text = await res.text(); append(`HTTP ${res.status}: ${text.slice(0, 500)}`); return; } const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); buf = parseNdjsonChunk(buf, (obj) => { if ( typeof obj === "object" && obj !== null && "error" in obj && typeof (obj as { error?: string }).error === "string" ) { append(`[error] ${(obj as { error: string }).error}`); return; } if (isProgress(obj)) { append(obj.message); } else if ( typeof obj === "object" && obj !== null && (obj as { kind?: string }).kind === "classified" && typeof (obj as { message?: string }).message === "string" ) { append(`→ ${(obj as { message: string }).message}`); } else if ( typeof obj === "object" && obj !== null && (obj as { kind?: string }).kind === "error" && typeof (obj as { message?: string }).message === "string" ) { append(`[throw] ${(obj as { message: string }).message}`); } else if ( typeof obj === "object" && obj !== null && (obj as { kind?: string }).kind === "done" ) { const res = (obj as { result?: unknown }).result; setLastResult(res ?? null); append(`[done] ${JSON.stringify(res, null, 2)}`); } }); } if (buf.trim()) { try { const o = JSON.parse(buf) as unknown; if (isProgress(o)) append(o.message); else if ( typeof o === "object" && o !== null && (o as { kind?: string }).kind === "classified" && typeof (o as { message?: string }).message === "string" ) append(`→ ${(o as { message: string }).message}`); else if ( typeof o === "object" && o !== null && (o as { kind?: string }).kind === "error" && typeof (o as { message?: string }).message === "string" ) append(`[throw] ${(o as { message: string }).message}`); else if ( typeof o === "object" && o !== null && (o as { kind?: string }).kind === "done" ) { const res = (o as { result?: unknown }).result; setLastResult(res ?? null); append(`[done] ${JSON.stringify(res, null, 2)}`); } } catch { append(`[incomplete] ${buf.slice(0, 200)}`); } } } catch (e) { append(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }, [ append, dryRunEmail, force, ignoreRedisHistory, skipCredits, userId, ]); return (

Digest debugger

Manually runs the same digest pipeline as QStash →{" "} /api/internal/digest/process. Set{" "} DIGEST_TRACK_EMAILED_URLS=1 to dedupe emailed URLs in Redis; use “Ignore Redis history” to bypass that and inactive-engagement pause for repeat tests.

Live log
          {logLines.length === 0 ? "(no output yet)" : logLines.join("\n")}
        
{lastResult !== null ? (

Last result object is included in the log as{" "} [done].

) : null}
); }