;
+ const all: Evt[] = [];
+ for (const [, info] of Object.entries(monitors)) {
+ try {
+ const res = await fetch(`${BASE_URL}/v1/monitors/${info.monitorId}/events`, { headers: { "x-api-key": API_KEY }, cache: "no-store" });
+ if (!res.ok) continue;
+ const data = await res.json();
+ for (const evt of data.events || []) {
+ const c = evt.output?.content;
+ if (!c || typeof c !== "object") continue;
+ const basis = evt.output?.basis || [];
+ const citations = basis.flatMap((b: { citations?: { title?: string; url?: string }[] }) =>
+ (b.citations || []).map((x) => ({ title: x.title || "", url: x.url || "" }))).filter((x: {url: string}) => x.url).slice(0, 3);
+ all.push({
+ headline: c.headline || "", summary: c.summary || "", category: c.category || "",
+ severity: c.severity || "informational", eventDate: evt.event_date || "",
+ affectedEntities: c.affected_entities || "", monitorName: info.name, citations,
+ });
+ }
+ } catch {}
+ }
+ return all;
+}
+
+const SYSTEM = `You are the editor of "Datacenter Signal," a weekly intelligence brief for datacenter infrastructure investors. Transform the supplied monitor events into a polished HTML newsletter body.
+
+VOICE: Analytical, concise, data-anchored — like a Financial Times or Stratechery briefing. No hype, no speculation, no emoji.
+
+STRUCTURE:
+1. An issue line: Issue N — Week of DATE
+2. "The Week in One Read" — 2-3 sentence executive summary anchored on the editorial focus
+3. "Critical Developments" — 3-4 of the most important events (lead with ones matching the focus). For each: a category tag, a bold headline, and 2 paragraphs of analysis (what happened, stakeholders, implications, what to watch). Weave the provided source links inline using the publication name as anchor text.
+4. "Regional Roundup" — one line per active region
+5. "By the Numbers" — 6-10 key data points
+
+Use ONLY the supplied events as facts. Do not invent numbers, deals, or quotes beyond what the events state. If an event lacks a citation, state the fact without a link.
+
+HTML (inline styles only):
+- H2:
+- Body:
+- Links:
+- Bold:
+- Lists: -
+- Category tag: CATEGORY
+- Divider between the summary and Critical Developments:
+
+OUTPUT: Return ONLY the HTML body. No markdown, no code fences, no preamble.`;
+
+async function main() {
+ const issueNumber = parseInt(process.argv[2]);
+ const focus = process.argv[3] || "The most consequential developments of the week";
+ if (!issueNumber) { console.error("Pass [focus]"); process.exit(1); }
+
+ const existing = await list({ prefix: `newsletters/issue-${issueNumber}`, token: BLOB_TOKEN });
+ if (existing.blobs.length) {
+ const r = await fetch(existing.blobs[0].downloadUrl, { headers: { Authorization: `Bearer ${BLOB_TOKEN}` } });
+ if (r.ok && (await r.json()).content) { console.log(`[${issueNumber}] already done`); fs.writeFileSync(`/tmp/issue-${issueNumber}.done`, "ok"); return; }
+ }
+
+ console.log(`[${issueNumber}] Fetching monitor events...`);
+ const events = await fetchMonitorEvents();
+ const critical = events.filter((e) => e.severity === "critical");
+ const notable = events.filter((e) => e.severity === "notable");
+ const regions = new Map();
+ for (const e of events) { if (!regions.has(e.monitorName)) regions.set(e.monitorName, []); regions.get(e.monitorName)!.push(e); }
+ console.log(`[${issueNumber}] ${events.length} events (${critical.length} critical). Composing with Claude...`);
+
+ const evtBlock = (e: Evt) => {
+ let s = `- [${e.severity.toUpperCase()}] ${e.headline}\n Region: ${e.monitorName} | Category: ${e.category} | Date: ${e.eventDate}\n ${e.summary}`;
+ if (e.affectedEntities) s += `\n Affects: ${e.affectedEntities}`;
+ if (e.citations.length) s += `\n Sources: ${e.citations.map((c) => `${c.title} — ${c.url}`).join(" ; ")}`;
+ return s;
+ };
+
+ const userMsg = `Issue ${issueNumber} — Week of ${weekOf(issueNumber)}
+EDITORIAL FOCUS: ${focus}
+
+CRITICAL EVENTS:
+${critical.slice(0, 12).map(evtBlock).join("\n")}
+
+NOTABLE EVENTS:
+${notable.slice(0, 15).map(evtBlock).join("\n")}
+
+REGIONAL ACTIVITY (events per region):
+${Array.from(regions.entries()).sort((a, b) => b[1].length - a[1].length).slice(0, 14).map(([n, e]) => `- ${n}: ${e.length} events; lead: ${e[0].headline}`).join("\n")}
+
+TOTALS: ${events.length} events, ${critical.length} critical, ${regions.size} active markets.
+
+Write the HTML body for Issue ${issueNumber} now, leading with developments relevant to the editorial focus.`;
+
+ const anthropic = new Anthropic({ apiKey: ANTHROPIC_KEY });
+ const resp = await anthropic.messages.create({
+ model: "claude-sonnet-5", max_tokens: 16000, system: SYSTEM,
+ messages: [{ role: "user", content: userMsg }],
+ });
+ let bodyHtml = resp.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("\n").trim();
+ const fence = bodyHtml.match(/```html\s*([\s\S]*?)```/);
+ if (fence) bodyHtml = fence[1].trim();
+
+ if (bodyHtml.length < 1500) { console.error(`[${issueNumber}] output too short (${bodyHtml.length} chars):\n${bodyHtml}`); process.exit(1); }
+
+ const issueData = {
+ issueNumber, content: bodyHtml, emailHtml: wrapEmailTemplate(bodyHtml, issueNumber),
+ focus, weekOf: weekOf(issueNumber),
+ stats: { events: events.length, critical: critical.length, markets: regions.size },
+ generatedAt: new Date().toISOString(), status: "completed",
+ };
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify(issueData), {
+ access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN,
+ });
+ console.log(`[${issueNumber}] ✓ stored (${bodyHtml.length} chars)`);
+ fs.writeFileSync(`/tmp/issue-${issueNumber}.done`, "ok");
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/set-webhooks.ts b/typescript-recipes/parallel-datacenter-map/scripts/set-webhooks.ts
new file mode 100644
index 0000000..1b69718
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/set-webhooks.ts
@@ -0,0 +1,62 @@
+/**
+ * Updates all monitors to use webhook delivery.
+ * Pass the public URL as an argument.
+ *
+ * Usage: npx tsx scripts/set-webhooks.ts https://your-app.vercel.app
+ */
+
+import * as fs from "fs";
+
+const API_KEY =
+ process.env.PARALLEL_API_KEY;
+const BASE_URL = "https://api.parallel.ai";
+
+async function main() {
+ const appUrl = process.argv[2];
+ if (!appUrl) {
+ console.error("Usage: npx tsx scripts/set-webhooks.ts ");
+ console.error("Example: npx tsx scripts/set-webhooks.ts https://datacenter-map-demo.vercel.app");
+ process.exit(1);
+ }
+
+ const webhookUrl = `${appUrl.replace(/\/$/, "")}/api/webhook`;
+ console.log(`Setting webhook URL: ${webhookUrl}\n`);
+
+ const monitorsPath = "./src/data/monitors.json";
+ const monitors = JSON.parse(fs.readFileSync(monitorsPath, "utf-8"));
+
+ for (const [defId, info] of Object.entries(monitors) as [string, { monitorId: string; name: string }][]) {
+ try {
+ const res = await fetch(
+ `${BASE_URL}/v1/monitors/${info.monitorId}/update`,
+ {
+ method: "POST",
+ headers: {
+ "x-api-key": API_KEY,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ webhook: {
+ url: webhookUrl,
+ event_types: ["monitor.event.detected"],
+ },
+ }),
+ }
+ );
+
+ if (res.ok) {
+ console.log(` ✓ ${info.name}`);
+ } else {
+ const err = await res.text();
+ console.error(` ✗ ${info.name}: ${res.status} ${err.slice(0, 100)}`);
+ }
+ } catch (e) {
+ console.error(` ✗ ${info.name}: ${(e as Error).message}`);
+ }
+ }
+
+ console.log(`\nDone. All monitors will POST to ${webhookUrl}`);
+ console.log("Events will stream to clients via SSE at /api/webhook (GET)");
+}
+
+main().catch(console.error);
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/setup-monitors.ts b/typescript-recipes/parallel-datacenter-map/scripts/setup-monitors.ts
new file mode 100644
index 0000000..a360cfe
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/setup-monitors.ts
@@ -0,0 +1,113 @@
+/**
+ * Creates all monitors via the Parallel Monitor API with structured output.
+ * Saves monitor IDs to src/data/monitors.json for the app to use.
+ *
+ * Usage: npx tsx scripts/setup-monitors.ts
+ */
+
+import * as fs from "fs";
+import { MONITOR_DEFS, MONITOR_OUTPUT_SCHEMA } from "./monitor-configs";
+
+const API_KEY =
+ process.env.PARALLEL_API_KEY;
+const BASE_URL = "https://api.parallel.ai";
+
+async function createMonitor(def: (typeof MONITOR_DEFS)[number]) {
+ const metadata: Record = {
+ demo_id: def.id,
+ name: def.name,
+ class: def.class,
+ };
+ if (def.region) metadata.region = def.region;
+ if (def.facilityCode) metadata.facilityCode = def.facilityCode;
+ if (def.states) metadata.states = def.states.join(",");
+
+ const body = {
+ type: "event_stream",
+ frequency: def.frequency,
+ settings: {
+ query: def.query,
+ processor: def.processor,
+ output_schema: MONITOR_OUTPUT_SCHEMA,
+ },
+ metadata,
+ };
+
+ const res = await fetch(`${BASE_URL}/v1/monitors`, {
+ method: "POST",
+ headers: {
+ "x-api-key": API_KEY,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const err = await res.text();
+ throw new Error(
+ `Failed to create monitor ${def.id}: ${res.status} ${err}`
+ );
+ }
+
+ return res.json();
+}
+
+async function main() {
+ console.log(`Creating ${MONITOR_DEFS.length} monitors with structured output...\n`);
+
+ const results: Record<
+ string,
+ {
+ monitorId: string;
+ name: string;
+ class: string;
+ query: string;
+ frequency: string;
+ region?: string;
+ facilityCode?: string;
+ states?: string[];
+ }
+ > = {};
+
+ for (const def of MONITOR_DEFS) {
+ try {
+ const result = await createMonitor(def);
+ const monitorId = result.monitor_id;
+ results[def.id] = {
+ monitorId,
+ name: def.name,
+ class: def.class,
+ query: def.query,
+ frequency: def.frequency,
+ region: def.region,
+ facilityCode: def.facilityCode,
+ states: def.states,
+ };
+ console.log(
+ ` ✓ ${def.id.padEnd(28)} → ${monitorId} (${def.states?.join(",") || "national"})`
+ );
+ } catch (e) {
+ console.error(` ✗ ${def.id}:`, (e as Error).message);
+ }
+ }
+
+ const outPath = "./src/data/monitors.json";
+ fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
+
+ const regionCount = Object.values(results).filter(
+ (r) => r.class === "region"
+ ).length;
+ const facilityCount = Object.values(results).filter(
+ (r) => r.class === "facility"
+ ).length;
+ const discoveryCount = Object.values(results).filter(
+ (r) => r.class === "discovery"
+ ).length;
+
+ console.log(
+ `\nCreated ${Object.keys(results).length} monitors: ${regionCount} region, ${facilityCount} facility, ${discoveryCount} discovery`
+ );
+ console.log(`Saved to ${outPath}`);
+}
+
+main().catch(console.error);
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/snapshots-to-daily.ts b/typescript-recipes/parallel-datacenter-map/scripts/snapshots-to-daily.ts
new file mode 100644
index 0000000..a05d49a
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/snapshots-to-daily.ts
@@ -0,0 +1,55 @@
+#!/usr/bin/env npx tsx
+/**
+ * Recreate ONLY the currently-referenced snapshot monitors (src/data/snapshot-monitors.json)
+ * at daily (1d) frequency. The API has no PATCH, so we create replacements and rewrite the map.
+ * Scoped to the referenced set (not all 1,939 enrichment runs) to keep the demo's monitor
+ * footprint sane.
+ *
+ * Usage: PARALLEL_API_KEY=xxx npx tsx scripts/snapshots-to-daily.ts
+ */
+import * as fs from "fs";
+
+const API_KEY = process.env.PARALLEL_API_KEY;
+if (!API_KEY) { console.error("Set PARALLEL_API_KEY"); process.exit(1); }
+const BASE_URL = "https://api.parallel.ai";
+const WEBHOOK_URL = process.env.WEBHOOK_URL || "";
+const PATH = "./src/data/snapshot-monitors.json";
+const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+async function main() {
+ const current = JSON.parse(fs.readFileSync(PATH, "utf-8")) as Record;
+ const entries = Object.entries(current);
+ console.log(`Recreating ${entries.length} referenced snapshot monitors at 1d...`);
+
+ const out: typeof current = {};
+ let done = 0, failed = 0;
+ for (const [idx, snap] of entries) {
+ const res = await fetch(`${BASE_URL}/v1/monitors`, {
+ method: "POST",
+ headers: { "x-api-key": API_KEY!, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ type: "snapshot", frequency: "1d", processor: "base",
+ settings: { task_run_id: snap.runId },
+ ...(WEBHOOK_URL ? { webhook: { url: WEBHOOK_URL, event_types: ["monitor.event.detected"] } } : {}),
+ metadata: { facility_name: snap.facilityName.slice(0, 100), facility_index: idx, type: "datacenter-snapshot" },
+ }),
+ });
+ if (res.ok) {
+ const d = await res.json();
+ out[idx] = { monitorId: d.monitor_id, runId: snap.runId, facilityName: snap.facilityName };
+ done++;
+ } else {
+ out[idx] = snap; // keep old on failure so we never lose an entry
+ failed++;
+ }
+ if ((done + failed) % 25 === 0) {
+ fs.writeFileSync(PATH, JSON.stringify(out, null, 2));
+ process.stdout.write(`\r ${done} recreated, ${failed} failed / ${entries.length}`);
+ }
+ await sleep(250);
+ }
+ fs.writeFileSync(PATH, JSON.stringify(out, null, 2));
+ console.log(`\nDone: ${done} recreated at 1d, ${failed} failed (kept old).`);
+ fs.writeFileSync("/tmp/snapshots-daily.done", "ok");
+}
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/update-snapshots.ts b/typescript-recipes/parallel-datacenter-map/scripts/update-snapshots.ts
new file mode 100644
index 0000000..69078ff
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/update-snapshots.ts
@@ -0,0 +1,73 @@
+/**
+ * Recreates snapshot monitors with updated settings (1d frequency, base processor).
+ * The API doesn't support PATCH/PUT, so we create new monitors and update the IDs.
+ *
+ * Usage: PARALLEL_API_KEY=xxx npx tsx scripts/update-snapshots.ts
+ */
+
+import * as fs from "fs";
+
+const API_KEY = process.env.PARALLEL_API_KEY;
+if (!API_KEY) { console.error("Set PARALLEL_API_KEY"); process.exit(1); }
+
+const BASE_URL = "https://api.parallel.ai";
+const WEBHOOK_URL = process.env.WEBHOOK_URL || "";
+
+const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
+
+async function main() {
+ const runsPath = "./src/data/enrichment-v2-runs.json";
+ if (!fs.existsSync(runsPath)) { console.error("No enrichment-v2-runs.json"); process.exit(1); }
+
+ const snapshotPath = "./src/data/snapshot-monitors.json";
+ const runData = JSON.parse(fs.readFileSync(runsPath, "utf-8"));
+
+ console.log(`Recreating ${runData.runs.length} snapshot monitors (1d, base)...\n`);
+
+ const newSnapshots: Record = {};
+ let created = 0, failed = 0;
+
+ for (const run of runData.runs) {
+ const res = await fetch(`${BASE_URL}/v1/monitors`, {
+ method: "POST",
+ headers: { "x-api-key": API_KEY!, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ type: "snapshot",
+ frequency: "1d",
+ processor: "base",
+ settings: { task_run_id: run.runId },
+ ...(WEBHOOK_URL ? { webhook: { url: WEBHOOK_URL, event_types: ["monitor.event.detected"] } } : {}),
+ metadata: {
+ facility_name: run.facilityName.slice(0, 100),
+ facility_index: String(run.facilityIndex),
+ type: "datacenter-snapshot",
+ },
+ }),
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ newSnapshots[String(run.facilityIndex)] = {
+ monitorId: data.monitor_id,
+ runId: run.runId,
+ facilityName: run.facilityName,
+ };
+ created++;
+ } else {
+ failed++;
+ }
+
+ if ((created + failed) % 50 === 0) {
+ fs.writeFileSync(snapshotPath, JSON.stringify(newSnapshots, null, 2));
+ process.stdout.write(`\r Created: ${created}, Failed: ${failed} / ${runData.runs.length}`);
+ }
+
+ await sleep(300); // Rate limit
+ }
+
+ fs.writeFileSync(snapshotPath, JSON.stringify(newSnapshots, null, 2));
+ console.log(`\n\nDone: ${created} created, ${failed} failed`);
+ console.log(`Saved to ${snapshotPath}`);
+}
+
+main().catch(console.error);
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/upload-enrichments.ts b/typescript-recipes/parallel-datacenter-map/scripts/upload-enrichments.ts
new file mode 100644
index 0000000..4bafab7
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/upload-enrichments.ts
@@ -0,0 +1,50 @@
+/**
+ * Uploads the full enrichments.json to Vercel Blob.
+ * The compact version stays in git for table columns.
+ * The full version is fetched on-demand for basis/citations.
+ *
+ * Usage: BLOB_READ_WRITE_TOKEN=vercel_blob_xxx npx tsx scripts/upload-enrichments.ts
+ */
+
+import * as fs from "fs";
+import { put } from "@vercel/blob";
+
+async function main() {
+ const token = process.env.BLOB_READ_WRITE_TOKEN;
+ if (!token) {
+ console.error("Set BLOB_READ_WRITE_TOKEN env var first.");
+ console.error("Get it from: Vercel Dashboard → Project → Storage → Blob → Tokens");
+ process.exit(1);
+ }
+
+ const filePath = "./public/data/enrichments.json";
+ if (!fs.existsSync(filePath)) {
+ console.error("No enrichments.json found.");
+ process.exit(1);
+ }
+
+ const fileSize = fs.statSync(filePath).size;
+ console.log(`Uploading enrichments.json (${(fileSize / 1024 / 1024).toFixed(1)}MB)...`);
+
+ const fileBuffer = fs.readFileSync(filePath);
+
+ const blob = await put("enrichments.json", fileBuffer, {
+ access: "private",
+ allowOverwrite: true,
+ contentType: "application/json",
+ token,
+ });
+
+ console.log(`\nUploaded to: ${blob.url}`);
+ console.log(`\nSet this env var in Vercel:`);
+ console.log(` ENRICHMENTS_BLOB_URL=${blob.url}`);
+
+ // Also save the URL locally
+ fs.writeFileSync(".env.local",
+ fs.readFileSync(".env.local", "utf-8").replace(/ENRICHMENTS_BLOB_URL=.*/g, "").trim() +
+ `\nENRICHMENTS_BLOB_URL=${blob.url}\n`
+ );
+ console.log(`\nSaved to .env.local`);
+}
+
+main().catch(console.error);
diff --git a/typescript-recipes/parallel-datacenter-map/scripts/upload-per-facility.ts b/typescript-recipes/parallel-datacenter-map/scripts/upload-per-facility.ts
new file mode 100644
index 0000000..023c2c5
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/scripts/upload-per-facility.ts
@@ -0,0 +1,48 @@
+/**
+ * Splits enrichments.json into per-facility files and uploads to Vercel Blob.
+ * Each facility gets its own file: enrichments/0.json, enrichments/1.json, etc.
+ * The basis endpoint then fetches just the one file it needs (~95KB vs 181MB).
+ *
+ * Usage: BLOB_READ_WRITE_TOKEN=xxx npx tsx scripts/upload-per-facility.ts
+ */
+
+import * as fs from "fs";
+import { put } from "@vercel/blob";
+
+const TOKEN = process.env.BLOB_READ_WRITE_TOKEN;
+
+async function main() {
+ if (!TOKEN) {
+ console.error("Set BLOB_READ_WRITE_TOKEN env var.");
+ process.exit(1);
+ }
+
+ const data = JSON.parse(fs.readFileSync("./public/data/enrichments.json", "utf-8"));
+ const keys = Object.keys(data);
+ console.log(`Uploading ${keys.length} per-facility files to Vercel Blob...\n`);
+
+ let uploaded = 0;
+ const BATCH = 20;
+
+ for (let i = 0; i < keys.length; i += BATCH) {
+ const batch = keys.slice(i, i + BATCH);
+ await Promise.all(
+ batch.map(async (key) => {
+ const content = JSON.stringify(data[key]);
+ await put(`enrichments/${key}.json`, content, {
+ access: "private",
+ allowOverwrite: true,
+ contentType: "application/json",
+ token: TOKEN,
+ });
+ uploaded++;
+ })
+ );
+ process.stdout.write(`\r ${uploaded} / ${keys.length}`);
+ }
+
+ console.log(`\n\nDone. ${uploaded} facility files uploaded.`);
+ console.log("Basis endpoint can now fetch: enrichments/{facilityIndex}.json");
+}
+
+main().catch(console.error);
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/basis/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/basis/route.ts
new file mode 100644
index 0000000..6192e7e
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/basis/route.ts
@@ -0,0 +1,91 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+const BLOB_TOKEN = process.env.BLOB_READ_WRITE_TOKEN || "";
+// Derive blob store host from ENRICHMENTS_BLOB_URL (e.g., https://xxx.private.blob.vercel-storage.com/enrichments.json)
+const BLOB_STORE = (() => {
+ const url = process.env.ENRICHMENTS_BLOB_URL || "";
+ try { return new URL(url).host; } catch { return ""; }
+})();
+
+/**
+ * Fetches basis data for a single facility from Vercel Blob.
+ * Each facility is stored as its own ~95KB file: enrichments/{index}.json
+ * No more loading 181MB into memory.
+ */
+export async function GET(request: NextRequest) {
+ const facilityIndex = request.nextUrl.searchParams.get("facility");
+ const field = request.nextUrl.searchParams.get("field");
+
+ if (!facilityIndex) {
+ return NextResponse.json({ error: "Missing facility param" }, { status: 400 });
+ }
+
+ // Fetch this one facility's enrichment from Blob (~95KB)
+ let entry: {
+ enrichment?: Record;
+ basis?: { field?: string; reasoning?: string; citations?: { url?: string; title?: string; excerpts?: string[] }[] }[];
+ } | null = null;
+
+ try {
+ const url = `https://${BLOB_STORE}/enrichments/${facilityIndex}.json`;
+ const res = await fetch(url, {
+ headers: BLOB_TOKEN ? { Authorization: `Bearer ${BLOB_TOKEN}` } : {},
+ cache: "no-store",
+ });
+
+ if (res.ok) {
+ entry = await res.json();
+ }
+ } catch {
+ // Blob fetch failed
+ }
+
+ // Dev fallback: read from local file
+ if (!entry) {
+ try {
+ const fs = await import("fs");
+ const path = await import("path");
+ const filePath = path.join(process.cwd(), "public/data/enrichments.json");
+ if (fs.existsSync(filePath)) {
+ const all = JSON.parse(fs.readFileSync(filePath, "utf-8"));
+ entry = all[facilityIndex] || null;
+ }
+ } catch {}
+ }
+
+ if (!entry) {
+ return NextResponse.json({ citations: [], reasoning: "" });
+ }
+
+ const basis = entry.basis || [];
+
+ if (field) {
+ const fieldBasis = basis.filter((b) => b.field === field);
+ const reasoning = fieldBasis[0]?.reasoning || "";
+ const citations = fieldBasis.flatMap((b) =>
+ (b.citations || []).map((c) => ({
+ field: b.field || "",
+ url: c.url || "",
+ title: c.title || "Source",
+ excerpts: c.excerpts || [],
+ }))
+ );
+ return NextResponse.json({ citations, reasoning });
+ }
+
+ const citations = basis.flatMap((b) =>
+ (b.citations || []).map((c) => ({
+ field: b.field || "",
+ url: c.url || "",
+ title: c.title || "Source",
+ }))
+ );
+ const reasoning: Record = {};
+ for (const b of basis) {
+ if (b.field && b.reasoning) reasoning[b.field] = b.reasoning;
+ }
+
+ return NextResponse.json({ citations, reasoning });
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/cron/newsletter/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/cron/newsletter/route.ts
new file mode 100644
index 0000000..f9b254b
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/cron/newsletter/route.ts
@@ -0,0 +1,183 @@
+import { NextRequest, NextResponse } from "next/server";
+import { list, put } from "@vercel/blob";
+import { writeNewsletter, wrapEmailTemplate } from "@/lib/newsletter-writer";
+import {
+ fetchMonitorEvents,
+ getIssueNumber,
+ buildPrompt,
+} from "../../newsletter/generate/route";
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300;
+
+const API_KEY = process.env.PARALLEL_API_KEY || "";
+const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || "";
+const BLOB_TOKEN = process.env.BLOB_READ_WRITE_TOKEN || "";
+const BASE_URL = "https://api.parallel.ai";
+
+/**
+ * Cron-triggered newsletter pipeline. Each invocation advances the state:
+ *
+ * not_found → kick off research (Task API ultra-fast)
+ * researching → check if research done
+ * research done → run Claude agent writer → save
+ * writing → skip (another invocation is handling it)
+ * completed → skip
+ *
+ * Schedule: every 15 min on Mondays (vercel.json). Typical completion: ~30 min.
+ */
+export async function GET(request: NextRequest) {
+ // Verify Vercel Cron secret
+ const cronSecret = process.env.CRON_SECRET;
+ if (cronSecret && request.headers.get("authorization") !== `Bearer ${cronSecret}`) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ if (!API_KEY || !BLOB_TOKEN) {
+ return NextResponse.json({ error: "Missing API_KEY or BLOB_TOKEN" }, { status: 500 });
+ }
+
+ const issueNumber = getIssueNumber();
+
+ // ─── Check current state in Blob ───
+ let data: Record | null = null;
+ try {
+ const { blobs } = await list({ prefix: `newsletters/issue-${issueNumber}`, token: BLOB_TOKEN });
+ if (blobs.length > 0) {
+ const res = await fetch(blobs[0].downloadUrl, { headers: { Authorization: `Bearer ${BLOB_TOKEN}` } });
+ if (res.ok) data = await res.json();
+ }
+ } catch {}
+
+ // ─── Already complete ───
+ if (data?.content) {
+ return NextResponse.json({ action: "none", reason: "already_done", issueNumber });
+ }
+
+ // ─── Writing in progress (another invocation is handling it) ───
+ if (data?.phase === "writing") {
+ const writingStart = new Date((data.writingStartedAt as string) || "0").getTime();
+ if (Date.now() - writingStart < 6 * 60 * 1000) {
+ return NextResponse.json({ action: "waiting", phase: "writing", issueNumber });
+ }
+ // Stale writing — fall through to re-check research
+ }
+
+ // ─── Research in progress — check if done ───
+ if (data?.runId) {
+ const statusRes = await fetch(`${BASE_URL}/v1/tasks/runs/${data.runId}`, {
+ headers: { "x-api-key": API_KEY },
+ });
+ if (!statusRes.ok) {
+ return NextResponse.json({ action: "waiting", phase: "research", issueNumber });
+ }
+
+ const statusData = await statusRes.json();
+
+ if (statusData.status === "completed") {
+ // Research done — fetch result and trigger writing
+ const resultRes = await fetch(`${BASE_URL}/v1/tasks/runs/${data.runId}/result`, {
+ headers: { "x-api-key": API_KEY },
+ });
+ if (!resultRes.ok) {
+ return NextResponse.json({ action: "error", reason: "result_fetch_failed", issueNumber });
+ }
+
+ const result = await resultRes.json();
+ const rawContent = result.output?.content;
+ const research = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent) || "";
+ const interactionId = statusData.interaction_id || data.runId;
+
+ if (!ANTHROPIC_KEY) {
+ return NextResponse.json({ action: "error", reason: "no_anthropic_key", issueNumber });
+ }
+
+ // Mark as writing
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ ...data, phase: "writing", writingStartedAt: new Date().toISOString(),
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+
+ try {
+ // Fetch events for regional roundup
+ const events = await fetchMonitorEvents();
+ const regions = new Map();
+ for (const e of events) {
+ if (!regions.has(e.monitorName)) regions.set(e.monitorName, []);
+ regions.get(e.monitorName)!.push(e);
+ }
+
+ const bodyHtml = await writeNewsletter({
+ research,
+ interactionId: interactionId as string,
+ issueNumber,
+ eventsTotal: events.length,
+ criticalCount: events.filter((e) => e.severity === "critical").length,
+ marketsActive: regions.size,
+ regionSummaries: Array.from(regions.entries())
+ .map(([name, evts]) => `${name} (${evts.length} events): ${evts[0]?.headline || ""}`)
+ .join("\n"),
+ parallelApiKey: API_KEY,
+ anthropicApiKey: ANTHROPIC_KEY,
+ });
+
+ const emailHtml = wrapEmailTemplate(bodyHtml, issueNumber);
+ const issueData = {
+ issueNumber, content: bodyHtml, emailHtml,
+ generatedAt: new Date().toISOString(), status: "completed",
+ };
+
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify(issueData), {
+ access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN,
+ });
+
+ return NextResponse.json({ action: "completed", issueNumber });
+ } catch (error) {
+ console.error("[cron/newsletter] Writing failed:", error);
+ // Reset to research phase for retry on next cron invocation
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ ...data, phase: "research",
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+ return NextResponse.json({ action: "error", reason: "writing_failed", issueNumber });
+ }
+ }
+
+ if (statusData.status === "failed") {
+ return NextResponse.json({ action: "error", reason: "research_failed", issueNumber });
+ }
+
+ return NextResponse.json({ action: "waiting", phase: "research", issueNumber });
+ }
+
+ // ─── No issue started — kick off research ───
+ const events = await fetchMonitorEvents();
+ const prompt = buildPrompt(events);
+
+ const taskRes = await fetch(`${BASE_URL}/v1/tasks/runs`, {
+ method: "POST",
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ input: prompt,
+ task_spec: {
+ output_schema: {
+ type: "text",
+ description: "Comprehensive factual research with specific data points, dates, numbers, and source URLs for each critical event.",
+ },
+ },
+ processor: "ultra-fast",
+ }),
+ });
+
+ if (!taskRes.ok) {
+ return NextResponse.json({ action: "error", reason: "task_api_failed", issueNumber });
+ }
+
+ const task = await taskRes.json();
+
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ issueNumber, runId: task.run_id, status: "generating", phase: "research",
+ startedAt: new Date().toISOString(),
+ stats: { events: events.length, critical: events.filter((e) => e.severity === "critical").length },
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+
+ return NextResponse.json({ action: "started", phase: "research", runId: task.run_id, issueNumber });
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/monitors/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/monitors/route.ts
new file mode 100644
index 0000000..11c1893
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/monitors/route.ts
@@ -0,0 +1,144 @@
+import { NextResponse } from "next/server";
+import type { Monitor, MonitorDetection, MonitorCategory } from "@/lib/types";
+import { STATE_TO_MONITOR, CA_SPLIT_LAT } from "@/lib/constants";
+import monitorsData from "@/data/monitors.json";
+import datacentersData from "../../../../public/data/datacenters.json";
+
+// Force dynamic — no caching, always fresh monitor events
+export const dynamic = "force-dynamic";
+
+const API_KEY = process.env.PARALLEL_API_KEY || "";
+const BASE_URL = "https://api.parallel.ai";
+
+interface MonitorInfo {
+ monitorId: string;
+ name: string;
+ class: "region" | "facility" | "discovery";
+ query: string;
+ frequency: string;
+ region?: string;
+ facilityCode?: string;
+ states?: string[];
+}
+
+/** Count how many datacenters each monitor covers */
+function computeFacilityCounts(): Record {
+ const dcs = datacentersData as { state: string; lat: number }[];
+ const counts: Record = {};
+
+ for (const dc of dcs) {
+ let monitorId = STATE_TO_MONITOR[dc.state];
+ if (dc.state === "CA" && dc.lat < CA_SPLIT_LAT) {
+ monitorId = "region-socal";
+ }
+ if (monitorId) {
+ counts[monitorId] = (counts[monitorId] || 0) + 1;
+ }
+ }
+ return counts;
+}
+
+const VALID_CATEGORIES: MonitorCategory[] = [
+ "POWER_GRID", "ZONING_POLICY", "COMMUNITY", "WATER",
+ "LAND_SUPPLY", "TENANT_DEMAND", "CAPITAL_OWNERSHIP", "CONSTRUCTION",
+];
+
+async function fetchMonitorEvents(monitorId: string): Promise {
+ if (!API_KEY) return [];
+
+ try {
+ const res = await fetch(
+ `${BASE_URL}/v1/monitors/${monitorId}/events`,
+ {
+ headers: { "x-api-key": API_KEY },
+ cache: "no-store",
+ }
+ );
+
+ if (!res.ok) return [];
+
+ const data = await res.json();
+ const rawEvents = data.events || [];
+ const detections: MonitorDetection[] = [];
+
+ for (const evt of rawEvents) {
+ const output = evt.output || {};
+ const content = output.content;
+ const citations =
+ output.basis?.[0]?.citations ||
+ output.basis?.flatMap(
+ (b: { citations?: { title: string; url: string }[] }) => b.citations || []
+ ) || [];
+
+ if (content && typeof content === "object" && content.category) {
+ detections.push({
+ eventId: evt.event_id,
+ eventDate: evt.event_date || new Date().toISOString(),
+ category: VALID_CATEGORIES.includes(content.category) ? content.category : "ZONING_POLICY",
+ headline: content.headline || "",
+ summary: content.summary || "",
+ severity: content.severity || "informational",
+ affectedEntities: content.affected_entities || "",
+ citations: citations.slice(0, 4),
+ rawPayload: evt,
+ });
+ } else if (content && typeof content === "string") {
+ detections.push({
+ eventId: evt.event_id,
+ eventDate: evt.event_date || new Date().toISOString(),
+ category: "ZONING_POLICY",
+ headline: content.split(/[.!?]/)[0]?.trim().slice(0, 120) || "New signal",
+ summary: content,
+ severity: "notable",
+ affectedEntities: "",
+ citations: citations.slice(0, 4),
+ rawPayload: evt,
+ });
+ }
+ }
+
+ return detections;
+ } catch {
+ return [];
+ }
+}
+
+export async function GET() {
+ try {
+ const monitors = monitorsData as Record;
+ const facilityCounts = computeFacilityCounts();
+
+ // Fetch events for all monitors in parallel
+ const entries = Object.entries(monitors);
+ const eventResults = await Promise.all(
+ entries.map(([, info]) => fetchMonitorEvents(info.monitorId))
+ );
+
+ const result: Monitor[] = entries.map(([defId, info], i) => ({
+ id: defId,
+ monitorId: info.monitorId,
+ name: info.name,
+ class: info.class,
+ query: info.query,
+ frequency: info.frequency || "1h",
+ region: info.region,
+ facilityCode: info.facilityCode,
+ states: info.states,
+ facilityCount: facilityCounts[defId] || 0,
+ events: eventResults[i],
+ }));
+
+ // Sort: monitors with events first
+ const classOrder = { region: 0, facility: 1, discovery: 2 };
+ result.sort((a, b) => {
+ if (a.events.length > 0 && b.events.length === 0) return -1;
+ if (a.events.length === 0 && b.events.length > 0) return 1;
+ return classOrder[a.class] - classOrder[b.class];
+ });
+
+ return NextResponse.json(result);
+ } catch (e) {
+ console.error("Failed to load monitors:", e);
+ return NextResponse.json([]);
+ }
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/generate/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/generate/route.ts
new file mode 100644
index 0000000..d3f66b7
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/generate/route.ts
@@ -0,0 +1,192 @@
+import { NextResponse } from "next/server";
+import { put, list } from "@vercel/blob";
+import monitorsData from "@/data/monitors.json";
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300; // 5 min timeout for Vercel
+
+const API_KEY = process.env.PARALLEL_API_KEY || "";
+const BLOB_TOKEN = process.env.BLOB_READ_WRITE_TOKEN || "";
+const BASE_URL = "https://api.parallel.ai";
+
+function getIssueNumber() {
+ return Math.floor((Date.now() - new Date("2024-01-01").getTime()) / (7 * 24 * 60 * 60 * 1000));
+}
+
+async function fetchMonitorEvents() {
+ const monitors = monitorsData as Record;
+ const allEvents: { headline: string; summary: string; category: string; severity: string; eventDate: string; affectedEntities: string; monitorName: string; citations: { title: string; url: string }[] }[] = [];
+
+ for (const [, info] of Object.entries(monitors)) {
+ try {
+ const res = await fetch(`${BASE_URL}/v1/monitors/${info.monitorId}/events`, {
+ headers: { "x-api-key": API_KEY }, cache: "no-store",
+ });
+ if (!res.ok) continue;
+ const data = await res.json();
+ for (const evt of data.events || []) {
+ const content = evt.output?.content;
+ if (!content || typeof content !== "object") continue;
+ const basis = evt.output?.basis || [];
+ const citations = basis.flatMap((b: { citations?: { title?: string; url?: string }[] }) =>
+ (b.citations || []).map((c) => ({ title: c.title || "", url: c.url || "" }))
+ ).slice(0, 3);
+ allEvents.push({
+ headline: content.headline || "", summary: content.summary || "",
+ category: content.category || "", severity: content.severity || "informational",
+ eventDate: evt.event_date || "", affectedEntities: content.affected_entities || "",
+ monitorName: info.name, citations,
+ });
+ }
+ } catch {}
+ }
+
+ return allEvents;
+}
+
+function buildPrompt(events: Awaited>) {
+ const critical = events.filter((e) => e.severity === "critical").slice(0, 5);
+ const regions = new Map();
+ for (const e of events) {
+ if (!regions.has(e.monitorName)) regions.set(e.monitorName, []);
+ regions.get(e.monitorName)!.push(e);
+ }
+ const issueNumber = getIssueNumber();
+
+ let criticalSection = "";
+ for (const evt of critical) {
+ criticalSection += `\n### ${evt.headline}\nRegion: ${evt.monitorName} | Category: ${evt.category} | Date: ${evt.eventDate}\n${evt.summary}\n`;
+ if (evt.affectedEntities) criticalSection += `Affects: ${evt.affectedEntities}\n`;
+ if (evt.citations.length > 0) criticalSection += `Sources: ${evt.citations.map((c) => `${c.title} (${c.url})`).join("; ")}\n`;
+ }
+
+ let regionalSection = "";
+ for (const [name, evts] of Array.from(regions.entries()).slice(0, 12)) {
+ regionalSection += `- **${name}** (${evts.length} events): ${evts[0].headline}\n`;
+ }
+
+ return `Write "Datacenter Signal — Issue ${issueNumber}", a weekly infrastructure intelligence brief for datacenter investors.
+
+CRITICAL EVENTS THIS WEEK (deep-research each one):
+${criticalSection || "No critical events this week."}
+
+ALL EVENTS SUMMARY:
+- Total events detected: ${events.length}
+- Critical: ${critical.length}
+- Markets with activity: ${regions.size}
+
+REGIONAL ACTIVITY:
+${regionalSection || "No regional activity."}
+
+INSTRUCTIONS:
+1. Open with "The week in one read" — 2-3 sentence executive summary
+2. "Critical developments" — thorough analysis of each critical event with background, stakeholders, implications, and what to watch
+3. "Regional roundup" — one line per active region
+4. "By the numbers" — key stats
+
+Tone: analytical, concise, data-anchored. Like a Financial Times briefing. No hype, no speculation.
+Format: clean markdown with ## headers, bullet points, and [inline source links](url). No emoji.`;
+}
+
+function markdownToEmailHtml(md: string): string {
+ let html = md;
+ html = html.replace(/&/g, "&");
+
+ // Convert [N] refs to inline links if references section exists
+ const refSection = html.split("## References");
+ if (refSection.length > 1) {
+ const refs: Record = {};
+ for (const line of refSection[1].split("\n")) {
+ const m = line.match(/^(\d+)\.\s+\*(.+?)\*\.\s+(https?:\/\/\S+)/);
+ if (m) refs[m[1]] = { title: m[2], url: m[3] };
+ }
+ html = refSection[0];
+ html = html.replace(/\[(\d+)\]/g, (_, num) => {
+ const ref = refs[num];
+ if (ref) {
+ const domain = ref.url.split("/")[2]?.replace("www.", "").split(".")[0] || "source";
+ return `(${domain})`;
+ }
+ return `[${num}]`;
+ });
+ }
+
+ html = html.replace(/^### (.+)$/gm, '
$1
');
+ html = html.replace(/^## (.+)$/gm, '$1
');
+ html = html.replace(/^# (.+)$/gm, '$1
');
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ html = html.replace(/(?$1');
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+ html = html.replace(/^- (.+)$/gm, '- $1
');
+ html = html.replace(/((?:- ]*>.*?<\/li>\n?)+)/g, '');
+ html = html.replace(/\n\n/g, '
');
+ html = html.replace(/\n/g, "
");
+ html = '
' + html + "
";
+
+ return `
+
+
parallel
+
+Datacenter Signal
+Issue ${getIssueNumber()}
+
+
${html}
+
+
parallel
+
hello@parallel.ai · Palo Alto, CA
+
`;
+}
+
+// POST: kick off newsletter generation — returns immediately with runId
+// The preview endpoint polls for completion and finalizes (save + send)
+export async function POST() {
+ if (!API_KEY) return NextResponse.json({ error: "No API key" }, { status: 500 });
+
+ const issueNumber = getIssueNumber();
+
+ // Check if already generated
+ if (BLOB_TOKEN) {
+ try {
+ const { blobs } = await list({ prefix: `newsletters/issue-${issueNumber}`, token: BLOB_TOKEN });
+ if (blobs.length > 0) {
+ const res = await fetch(blobs[0].downloadUrl, { headers: { Authorization: `Bearer ${BLOB_TOKEN}` } });
+ if (res.ok) {
+ const existing = await res.json();
+ if (existing.content) return NextResponse.json({ status: "already_generated", ...existing });
+ // If we have a runId but no content, it's still generating
+ if (existing.runId) return NextResponse.json({ status: "generating", runId: existing.runId, issueNumber });
+ }
+ }
+ } catch {}
+ }
+
+ // Fetch monitor events and kick off Task API
+ const events = await fetchMonitorEvents();
+ const prompt = buildPrompt(events);
+
+ const taskRes = await fetch(`${BASE_URL}/v1/tasks/runs`, {
+ method: "POST",
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ input: prompt,
+ task_spec: { output_schema: { type: "text", description: "Markdown-formatted weekly datacenter intelligence brief with inline citations." } },
+ processor: "ultra-fast",
+ }),
+ });
+
+ if (!taskRes.ok) return NextResponse.json({ error: await taskRes.text() }, { status: 500 });
+ const task = await taskRes.json();
+
+ // Save the pending state to Blob immediately (so we don't re-kick on next request)
+ if (BLOB_TOKEN) {
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ issueNumber, runId: task.run_id, status: "generating", phase: "research", startedAt: new Date().toISOString(),
+ stats: { events: events.length, critical: events.filter((e) => e.severity === "critical").length },
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+ }
+
+ return NextResponse.json({ status: "generating", runId: task.run_id, issueNumber });
+}
+
+// Exported for use by the preview route
+export { markdownToEmailHtml, getIssueNumber, fetchMonitorEvents, buildPrompt };
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/issues/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/issues/route.ts
new file mode 100644
index 0000000..eef651d
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/issues/route.ts
@@ -0,0 +1,63 @@
+import { NextResponse } from "next/server";
+import { list } from "@vercel/blob";
+import { getIssueNumber } from "../generate/route";
+
+export const dynamic = "force-dynamic";
+
+const BLOB_TOKEN = process.env.BLOB_READ_WRITE_TOKEN || "";
+
+function weekOf(issueNumber: number): string {
+ const ms = new Date("2024-01-01").getTime() + issueNumber * 7 * 24 * 60 * 60 * 1000;
+ return new Date(ms).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
+}
+
+interface IssueMeta {
+ issueNumber: number;
+ weekOf: string;
+ hasContent: boolean;
+ focus?: string;
+ generatedAt?: string;
+ stats?: { events?: number; critical?: number; markets?: number };
+ isCurrent: boolean;
+}
+
+// GET: list every generated issue in the archive (newest first)
+export async function GET() {
+ const currentIssue = getIssueNumber();
+
+ if (!BLOB_TOKEN) {
+ return NextResponse.json({ currentIssue, issues: [] });
+ }
+
+ try {
+ const { blobs } = await list({ prefix: "newsletters/issue-", token: BLOB_TOKEN });
+ const issues: IssueMeta[] = [];
+
+ await Promise.all(
+ blobs.map(async (b) => {
+ const m = b.pathname.match(/issue-(\d+)\.json$/);
+ if (!m) return;
+ const issueNumber = parseInt(m[1]);
+ try {
+ const res = await fetch(b.downloadUrl, { headers: { Authorization: `Bearer ${BLOB_TOKEN}` } });
+ if (!res.ok) return;
+ const d = await res.json();
+ issues.push({
+ issueNumber,
+ weekOf: d.weekOf || weekOf(issueNumber),
+ hasContent: !!d.content,
+ focus: d.focus,
+ generatedAt: d.generatedAt,
+ stats: d.stats,
+ isCurrent: issueNumber === currentIssue,
+ });
+ } catch {}
+ })
+ );
+
+ issues.sort((a, b) => b.issueNumber - a.issueNumber);
+ return NextResponse.json({ currentIssue, issues });
+ } catch {
+ return NextResponse.json({ currentIssue, issues: [] });
+ }
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/preview/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/preview/route.ts
new file mode 100644
index 0000000..314739b
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/newsletter/preview/route.ts
@@ -0,0 +1,144 @@
+import { NextRequest, NextResponse } from "next/server";
+import { list, put } from "@vercel/blob";
+import { writeNewsletter, wrapEmailTemplate } from "@/lib/newsletter-writer";
+import {
+ fetchMonitorEvents,
+ getIssueNumber,
+ markdownToEmailHtml,
+} from "../generate/route";
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300; // 5 min — needed for Claude agent writing phase
+
+const API_KEY = process.env.PARALLEL_API_KEY || "";
+const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || "";
+const BLOB_TOKEN = process.env.BLOB_READ_WRITE_TOKEN || "";
+const BASE_URL = "https://api.parallel.ai";
+
+// GET: fetch latest issue — handles two-phase pipeline (research → writing)
+export async function GET(request: NextRequest) {
+ const issueParam = request.nextUrl.searchParams.get("issue");
+ const issueNumber = issueParam ? parseInt(issueParam) : getIssueNumber();
+
+ if (!BLOB_TOKEN) return NextResponse.json({ status: "not_found", issueNumber });
+
+ try {
+ const { blobs } = await list({ prefix: `newsletters/issue-${issueNumber}`, token: BLOB_TOKEN });
+ if (blobs.length === 0) return NextResponse.json({ status: "not_found", issueNumber });
+
+ const res = await fetch(blobs[0].downloadUrl, { headers: { Authorization: `Bearer ${BLOB_TOKEN}` } });
+ if (!res.ok) return NextResponse.json({ status: "not_found", issueNumber });
+
+ const data = await res.json();
+
+ // Already complete
+ if (data.content) return NextResponse.json({ ...data, status: "found" });
+
+ // Writing phase — Claude agent is working (triggered by another request)
+ if (data.phase === "writing") {
+ // If writing has been running for >6 min, it likely timed out — allow retry
+ const writingStart = new Date(data.writingStartedAt || 0).getTime();
+ if (Date.now() - writingStart < 6 * 60 * 1000) {
+ return NextResponse.json({ status: "generating", issueNumber });
+ }
+ // Stale writing phase — fall through to re-check research
+ }
+
+ // Research phase — check if Task API deep research completed
+ if (data.runId && API_KEY) {
+ const statusRes = await fetch(`${BASE_URL}/v1/tasks/runs/${data.runId}`, {
+ headers: { "x-api-key": API_KEY },
+ });
+ if (!statusRes.ok) return NextResponse.json({ status: "generating", issueNumber });
+
+ const statusData = await statusRes.json();
+
+ if (statusData.status === "completed") {
+ // Fetch research result
+ const resultRes = await fetch(`${BASE_URL}/v1/tasks/runs/${data.runId}/result`, {
+ headers: { "x-api-key": API_KEY },
+ });
+ if (!resultRes.ok) return NextResponse.json({ status: "generating", issueNumber });
+
+ const result = await resultRes.json();
+ const rawContent = result.output?.content;
+ const research = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent) || "";
+ const interactionId = statusData.interaction_id || data.runId;
+
+ if (ANTHROPIC_KEY) {
+ // ─── Agent approach: Claude writes the newsletter ───
+ // Mark as writing to prevent duplicate triggers
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ ...data, phase: "writing", writingStartedAt: new Date().toISOString(),
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+
+ try {
+ // Fetch current events for regional roundup
+ const events = await fetchMonitorEvents();
+ const regions = new Map();
+ for (const e of events) {
+ if (!regions.has(e.monitorName)) regions.set(e.monitorName, []);
+ regions.get(e.monitorName)!.push(e);
+ }
+
+ const bodyHtml = await writeNewsletter({
+ research,
+ interactionId,
+ issueNumber,
+ eventsTotal: events.length,
+ criticalCount: events.filter((e) => e.severity === "critical").length,
+ marketsActive: regions.size,
+ regionSummaries: Array.from(regions.entries())
+ .map(([name, evts]) => `${name} (${evts.length} events): ${evts[0]?.headline || ""}`)
+ .join("\n"),
+ parallelApiKey: API_KEY,
+ anthropicApiKey: ANTHROPIC_KEY,
+ });
+
+ const emailHtml = wrapEmailTemplate(bodyHtml, issueNumber);
+ const issueData = {
+ issueNumber, content: bodyHtml, emailHtml,
+ generatedAt: new Date().toISOString(), status: "completed",
+ };
+
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify(issueData), {
+ access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN,
+ });
+
+ return NextResponse.json({ ...issueData, status: "found" });
+ } catch (error) {
+ console.error("[newsletter] Writing failed:", error);
+ // Reset to research phase so next poll can retry
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify({
+ ...data, phase: "research",
+ }), { access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN });
+ return NextResponse.json({ status: "generating", issueNumber });
+ }
+ } else {
+ // ─── Fallback: markdown-to-HTML (no Anthropic key) ───
+ const emailHtml = markdownToEmailHtml(research);
+ const issueData = {
+ ...data, content: research, emailHtml,
+ generatedAt: new Date().toISOString(), status: "completed",
+ };
+
+ await put(`newsletters/issue-${issueNumber}.json`, JSON.stringify(issueData), {
+ access: "private", allowOverwrite: true, contentType: "application/json", token: BLOB_TOKEN,
+ });
+
+ return NextResponse.json({ ...issueData, status: "found" });
+ }
+ }
+
+ if (statusData.status === "failed") {
+ return NextResponse.json({ status: "not_found", issueNumber, error: "Research failed" });
+ }
+
+ return NextResponse.json({ status: "generating", issueNumber, runId: data.runId });
+ }
+
+ return NextResponse.json({ status: "generating", issueNumber });
+ } catch {
+ return NextResponse.json({ status: "not_found", issueNumber });
+ }
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/snapshots/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/snapshots/route.ts
new file mode 100644
index 0000000..4ee877d
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/snapshots/route.ts
@@ -0,0 +1,123 @@
+import { NextResponse } from "next/server";
+import snapshotData from "@/data/snapshot-monitors.json";
+
+export const dynamic = "force-dynamic";
+
+const API_KEY = process.env.PARALLEL_API_KEY || "";
+const BASE_URL = "https://api.parallel.ai";
+
+const snapshotMonitors = snapshotData as Record;
+
+export interface FieldBasis {
+ reasoning: string;
+ citations: { url: string; title: string }[];
+}
+
+export interface SnapshotUpdate {
+ facilityIndex: string;
+ facilityName: string;
+ monitorId: string;
+ timestamp: string;
+ changedFields: string[];
+ changes: Record;
+ /** Per changed field: why it changed + supporting sources (from the re-verification run). */
+ basis: Record;
+}
+
+// Cache results for 30s
+let cachedUpdates: SnapshotUpdate[] | null = null;
+let cacheTime = 0;
+const CACHE_TTL = 30_000;
+
+async function fetchSnapshotEvents(monitorId: string) {
+ try {
+ const res = await fetch(
+ `${BASE_URL}/v1/monitors/${monitorId}/events`,
+ { headers: { "x-api-key": API_KEY }, cache: "no-store" }
+ );
+ if (!res.ok) return null;
+ const data = await res.json();
+ const events = data.events || [];
+ if (events.length === 0) return null;
+
+ const latest = events[0];
+ const changedContent = latest.changed_output?.content || {};
+ const previousContent = latest.previous_output?.content || {};
+ const changedFields = Object.keys(changedContent);
+ if (changedFields.length === 0) return null;
+
+ const changes: Record = {};
+ for (const field of changedFields) {
+ changes[field] = { from: previousContent[field], to: changedContent[field] };
+ }
+
+ // The re-verification run carries its own basis (why the value changed +
+ // sources). Key it by field so the UI can explain the update, not the
+ // stale pre-update enrichment.
+ const rawBasis = (latest.changed_output?.basis || []) as {
+ field?: string;
+ reasoning?: string;
+ citations?: { url?: string; title?: string }[];
+ }[];
+ const basis: Record = {};
+ for (const b of rawBasis) {
+ if (!b.field) continue;
+ basis[b.field] = {
+ reasoning: b.reasoning || "",
+ citations: (b.citations || [])
+ .filter((c) => c.url)
+ .map((c) => ({ url: c.url as string, title: c.title || "Source" })),
+ };
+ }
+
+ return { timestamp: latest.event_date || "", changedFields, changes, basis };
+ } catch {
+ return null;
+ }
+}
+
+export async function GET() {
+ if (!API_KEY) return NextResponse.json({ updates: [], total: 0 });
+
+ if (cachedUpdates && Date.now() - cacheTime < CACHE_TTL) {
+ return NextResponse.json({ updates: cachedUpdates, total: Object.keys(snapshotMonitors).length, cached: true });
+ }
+
+ try {
+ const entries = Object.entries(snapshotMonitors);
+ if (entries.length === 0) return NextResponse.json({ updates: [], total: 0 });
+
+ const BATCH = 30;
+ const updates: SnapshotUpdate[] = [];
+
+ for (let i = 0; i < entries.length && i < 300; i += BATCH) {
+ const batch = entries.slice(i, i + BATCH);
+ const results = await Promise.all(
+ batch.map(async ([idx, snap]) => {
+ const result = await fetchSnapshotEvents(snap.monitorId);
+ if (!result) return null;
+ return {
+ facilityIndex: idx,
+ facilityName: snap.facilityName,
+ monitorId: snap.monitorId,
+ timestamp: result.timestamp,
+ changedFields: result.changedFields,
+ changes: result.changes,
+ basis: result.basis,
+ };
+ })
+ );
+ for (const r of results) {
+ if (r) updates.push(r);
+ }
+ }
+
+ cachedUpdates = updates;
+ cacheTime = Date.now();
+
+ return NextResponse.json({ updates, total: entries.length, checked: Math.min(entries.length, 300) });
+ } catch (e) {
+ console.error("Snapshots error:", e);
+ return NextResponse.json({ updates: [], total: 0 });
+ }
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/api/webhook/route.ts b/typescript-recipes/parallel-datacenter-map/src/app/api/webhook/route.ts
new file mode 100644
index 0000000..5632cca
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/api/webhook/route.ts
@@ -0,0 +1,121 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export interface WebhookEvent {
+ monitorId: string;
+ eventId: string;
+ eventDate: string;
+ type: string;
+ content: unknown;
+ receivedAt: string;
+ // Snapshot-specific
+ facilityIndex?: string;
+ facilityName?: string;
+ changedFields?: string[];
+}
+
+// In-memory stores
+const recentEvents: WebhookEvent[] = [];
+const MAX_EVENTS = 500;
+
+// Track snapshot updates by facility index
+const snapshotUpdates: Record = {};
+
+const clients = new Set();
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const metadata = body.data?.metadata || {};
+ const isSnapshot = metadata.type === "datacenter-snapshot";
+
+ // Extract changed fields from snapshot event
+ const changedOutput = body.data?.event?.changed_output;
+ const changedFields = changedOutput?.content
+ ? Object.keys(changedOutput.content)
+ : [];
+
+ const event: WebhookEvent = {
+ monitorId: body.data?.monitor_id || "",
+ eventId: body.data?.event?.event_id || body.data?.event?.event_group_id || "",
+ eventDate: body.data?.event?.event_date || new Date().toISOString(),
+ type: body.type || "unknown",
+ content: isSnapshot
+ ? { changed: changedOutput?.content, previous: body.data?.event?.previous_output?.content }
+ : body.data?.event?.output?.content || body.data,
+ receivedAt: new Date().toISOString(),
+ facilityIndex: metadata.facility_index,
+ facilityName: metadata.facility_name,
+ changedFields,
+ };
+
+ recentEvents.unshift(event);
+ if (recentEvents.length > MAX_EVENTS) recentEvents.pop();
+
+ // Track snapshot updates per facility
+ if (isSnapshot && metadata.facility_index) {
+ snapshotUpdates[metadata.facility_index] = {
+ timestamp: new Date().toISOString(),
+ changedFields,
+ };
+ }
+
+ // Push to SSE clients
+ const message = `data: ${JSON.stringify(event)}\n\n`;
+ for (const controller of clients) {
+ try {
+ controller.enqueue(new TextEncoder().encode(message));
+ } catch {
+ clients.delete(controller);
+ }
+ }
+
+ console.log(
+ `[webhook] ${body.type} ${isSnapshot ? "SNAPSHOT" : "EVENT"} from ${event.monitorId}${
+ isSnapshot ? ` facility=${metadata.facility_name} changed=[${changedFields.join(",")}]` : ""
+ }`
+ );
+
+ return NextResponse.json({ received: true });
+ } catch (e) {
+ console.error("[webhook] Error:", e);
+ return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
+ }
+}
+
+// SSE endpoint
+export async function GET() {
+ const stream = new ReadableStream({
+ start(controller) {
+ clients.add(controller);
+
+ for (const event of recentEvents.slice(0, 10)) {
+ controller.enqueue(
+ new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`)
+ );
+ }
+
+ const heartbeat = setInterval(() => {
+ try {
+ controller.enqueue(new TextEncoder().encode(`: heartbeat\n\n`));
+ } catch {
+ clearInterval(heartbeat);
+ clients.delete(controller);
+ }
+ }, 30000);
+ },
+ cancel() {},
+ });
+
+ return new Response(stream, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache, no-transform",
+ Connection: "keep-alive",
+ },
+ });
+}
+
+// Expose snapshot updates for the monitors API
+export function getSnapshotUpdates(): Record {
+ return snapshotUpdates;
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/globals.css b/typescript-recipes/parallel-datacenter-map/src/app/globals.css
new file mode 100644
index 0000000..ba53c24
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/globals.css
@@ -0,0 +1,100 @@
+@import "tailwindcss";
+
+@theme inline {
+ --font-sans: 'Gerstner Programm', var(--font-geist-sans), Arial, Helvetica, sans-serif;
+ --font-mono: 'FT System Mono', var(--font-geist-mono), 'SF Mono', 'Fira Code', 'Courier New', monospace;
+
+ /* Brand palette */
+ --color-off-white: #F9F8F4;
+ --color-brand: #FB631B;
+ --color-brand-hover: #F4793F;
+ --color-orange-tint: #F79A6F;
+ --color-orange-light: #F9BC9F;
+ --color-orange-wash: #FCDDCF;
+ --color-index-black: #1D1B16;
+ --color-neural: #D8D0BF;
+
+ /* Greys */
+ --color-grey-100: #F6F6F6;
+ --color-grey-200: #EEEEEE;
+ --color-grey-300: #E5E5E5;
+ --color-grey-400: #D6D6D6;
+ --color-grey-500: #ADADAC;
+ --color-grey-600: #858483;
+ --color-grey-700: #5C5B59;
+ --color-grey-800: #434343;
+ --color-grey-900: #181818;
+
+ /* Semantic */
+ --color-success: #69BE78;
+ --color-error: #E14942;
+}
+
+body {
+ background: var(--color-off-white);
+ color: var(--color-index-black);
+ font-family: var(--font-sans);
+}
+
+/* Leaflet overrides */
+.leaflet-container {
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+}
+
+/* Popup must render above everything on the map */
+.leaflet-pane.leaflet-popup-pane {
+ z-index: 800 !important;
+}
+
+.leaflet-popup-content-wrapper {
+ border-radius: 4px;
+ padding: 0;
+ border: 1px solid var(--color-grey-300);
+ box-shadow: 0 4px 12px rgba(29, 27, 22, 0.1);
+}
+
+.leaflet-popup-content {
+ margin: 0;
+ font-family: var(--font-sans);
+ font-size: 13px;
+ line-height: 16px;
+ color: var(--color-index-black);
+}
+
+.leaflet-popup-close-button {
+ color: var(--color-grey-600) !important;
+ font-size: 18px !important;
+ padding: 4px 8px !important;
+}
+
+.leaflet-control-attribution {
+ font-size: 10px !important;
+ background: rgba(249, 248, 244, 0.8) !important;
+ color: var(--color-grey-600) !important;
+}
+
+.leaflet-control-attribution a {
+ color: var(--color-grey-700) !important;
+}
+
+/* Hover tooltip styling */
+.facility-tooltip {
+ background: white !important;
+ border: 1px solid var(--color-grey-300) !important;
+ border-radius: 4px !important;
+ padding: 6px 10px !important;
+ box-shadow: 0 2px 8px rgba(29, 27, 22, 0.08) !important;
+ font-family: var(--font-sans) !important;
+}
+
+.facility-tooltip::before {
+ border-top-color: var(--color-grey-300) !important;
+}
+
+/* Pulse animation for monitor status dot */
+@keyframes pulse-dot {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.25; }
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/icon.svg b/typescript-recipes/parallel-datacenter-map/src/app/icon.svg
new file mode 100644
index 0000000..6d837bf
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/icon.svg
@@ -0,0 +1,8 @@
+
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/layout.tsx b/typescript-recipes/parallel-datacenter-map/src/app/layout.tsx
new file mode 100644
index 0000000..20b3455
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/layout.tsx
@@ -0,0 +1,34 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Datacenter Monitor — Parallel",
+ description:
+ "Datacenter infrastructure monitoring powered by Parallel Monitor API",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/app/page.tsx b/typescript-recipes/parallel-datacenter-map/src/app/page.tsx
new file mode 100644
index 0000000..669c1da
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/app/page.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import dynamic from "next/dynamic";
+import type { DisplayStatus, Monitor } from "@/lib/types";
+import { useDatacenters } from "@/hooks/useDatacenters";
+import { useMonitors } from "@/hooks/useMonitors";
+import { Header } from "@/components/Header";
+import { Toolbar } from "@/components/Toolbar";
+import { MonitorPanel } from "@/components/MonitorPanel";
+import { DatasetTable } from "@/components/DatasetTable";
+import { NewsletterIssue } from "@/components/NewsletterIssue";
+
+const MapPanel = dynamic(() => import("@/components/MapPanel"), {
+ ssr: false,
+ loading: () => (
+
+ Loading map...
+
+ ),
+});
+
+type Tab = "map" | "dataset";
+type FilterKey = DisplayStatus | "all";
+
+export default function Home() {
+ const [activeTab, setActiveTab] = useState("map");
+ const [activeFilter, setActiveFilter] = useState("all");
+ const [selectedMonitor, setSelectedMonitor] = useState(null);
+ const [focusedLocation, setFocusedLocation] = useState<{ lat: number; lng: number } | null>(null);
+ const [showBrief, setShowBrief] = useState(false);
+ const [aiOnly, setAiOnly] = useState(false);
+
+ const { filtered, counts, aiCount, totalCount } = useDatacenters(activeFilter, "", aiOnly);
+ const { monitors, totalEvents, lastChecked, snapshotUpdates } = useMonitors();
+
+ const lastCheckedStr = lastChecked.toLocaleTimeString("en-US", {
+ hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true,
+ });
+
+ const handleMonitorSelect = useCallback(
+ (monitor: Monitor | null) => {
+ setSelectedMonitor((prev) => prev?.id === monitor?.id ? null : monitor);
+ }, []
+ );
+
+ const handleLocateEvent = useCallback((lat: number, lng: number) => {
+ setFocusedLocation({ lat, lng });
+ setTimeout(() => setFocusedLocation(null), 2000);
+ }, []);
+
+ return (
+
+
setShowBrief(true)}
+ />
+ { setActiveFilter(f); setSelectedMonitor(null); }}
+ trackedCount={filtered.length}
+ aiCount={aiCount}
+ totalCount={totalCount}
+ aiOnly={aiOnly}
+ onAiOnlyChange={setAiOnly}
+ />
+
+
+ {activeTab === "map" ? (
+ <>
+
+
+
+
+ setShowBrief(true)}
+ />
+
+ >
+ ) : (
+
+
+
+ )}
+
+
+ {/* Weekly brief modal */}
+ {showBrief && setShowBrief(false)} />}
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/BasisPanel.tsx b/typescript-recipes/parallel-datacenter-map/src/components/BasisPanel.tsx
new file mode 100644
index 0000000..30e7a24
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/BasisPanel.tsx
@@ -0,0 +1,310 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { ExternalLink, X, Code, Loader2, RefreshCw } from "lucide-react";
+import { CopyCodeBlock } from "./CopyCodeBlock";
+import { timeAgo } from "@/lib/utils";
+
+function fmtValue(v: unknown): string {
+ if (v === undefined || v === null || v === "") return "—";
+ if (typeof v === "object") return JSON.stringify(v);
+ return String(v);
+}
+
+export interface BasisPanelData {
+ field: string;
+ value: string;
+ facilityName: string;
+ facilityIndex: number;
+ citations: { field: string; url: string; title: string }[];
+ reasoning?: string;
+ /** "ai" = AI classification (client-side data, skip the enrichment fetch) */
+ source?: "enrichment" | "ai";
+ /** Present when a snapshot re-verification changed this field. Carries the
+ * re-verification run's own reasoning + sources (which explain the NEW
+ * value, unlike the stale pre-update enrichment basis). */
+ update?: {
+ from?: unknown;
+ to?: unknown;
+ timestamp?: string;
+ reasoning?: string;
+ citations?: { field: string; url: string; title: string }[];
+ };
+}
+
+interface FullBasis {
+ citations: { field: string; url: string; title: string; excerpts?: string[] }[];
+ reasoning: string;
+}
+
+interface BasisPanelProps {
+ data: BasisPanelData | null;
+ onClose: () => void;
+}
+
+export function BasisPanel({ data, onClose }: BasisPanelProps) {
+ const [showCode, setShowCode] = useState(false);
+ const [fullBasis, setFullBasis] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ // Fetch full basis from API when panel opens or data changes.
+ // AI classifications carry their own citations client-side, and
+ // snapshot-updated fields carry the re-verification run's basis — in both
+ // cases skip the enrichment-blob fetch (which describes the OLD value).
+ useEffect(() => {
+ if (!data || data.source === "ai" || data.update) {
+ setFullBasis(null);
+ setLoading(false);
+ return;
+ }
+
+ setLoading(true);
+ setFullBasis(null);
+
+ fetch(`/api/basis?facility=${data.facilityIndex}&field=${encodeURIComponent(data.field)}`)
+ .then((r) => r.json())
+ .then((d) => {
+ setFullBasis(d);
+ setLoading(false);
+ })
+ .catch(() => setLoading(false));
+ }, [data?.facilityIndex, data?.field, data]);
+
+ if (!data) return null;
+
+ // For a snapshot-updated field, the current value IS update.to and the
+ // reasoning/citations come from the re-verification run — never the stale
+ // enrichment basis.
+ const displayValue =
+ data.update && data.update.to !== undefined
+ ? fmtValue(data.update.to)
+ : data.value || "—";
+ const reasoning = data.update?.reasoning || fullBasis?.reasoning || data.reasoning || "";
+ const citations =
+ data.update?.citations ||
+ fullBasis?.citations ||
+ data.citations.filter((c) => c.field === data.field || c.field === "");
+ const uniqueCitations = Array.from(
+ new Map(citations.map((c) => [c.url, c])).values()
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ {data.field.replace(/_/g, " ")}
+
+
+ {data.facilityName}
+
+
+
+
+
+ {/* Value */}
+
+
+
+ Value
+
+ {data.update && (
+
+ Updated
+
+ )}
+
+
+ {displayValue}
+
+
+
+ {/* Snapshot re-verification change */}
+ {data.update && (
+
+
+
+
+ Updated by snapshot re-verification
+
+ {data.update.timestamp && (
+ {timeAgo(data.update.timestamp)}
+ )}
+
+ {data.update.from !== undefined || data.update.to !== undefined ? (
+
+
+
Previous value
+
{fmtValue(data.update.from)}
+
+
+ ↓
+
+
+
Current value
+
{fmtValue(data.update.to)}
+
+
+ ) : (
+
+ This field changed in the most recent snapshot re-verification.
+
+ )}
+
+ )}
+
+ {/* Scrollable body */}
+
+ {loading ? (
+
+
+
+ Loading basis...
+
+
+ ) : (
+ <>
+ {/* Reasoning */}
+ {reasoning && (
+
+
+ Reasoning
+
+
+ {reasoning}
+
+
+ )}
+
+ {/* Citations */}
+ {uniqueCitations.length > 0 && (
+
+
+ Citations ({uniqueCitations.length})
+
+
+
+ )}
+ >
+ )}
+
+
+ {/* Footer */}
+
+
+
+ {data.source === "ai" ? "Classified by Task API" : data.update ? "Re-verified by Task API" : "Enriched by Task API"}
+
+
+
+ {showCode && (
+
+ {data.source === "ai" ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/CopyCodeBlock.tsx b/typescript-recipes/parallel-datacenter-map/src/components/CopyCodeBlock.tsx
new file mode 100644
index 0000000..2f4da4a
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/CopyCodeBlock.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useState } from "react";
+import { Copy, Check } from "lucide-react";
+
+interface CopyCodeBlockProps {
+ code: string;
+ label?: string;
+}
+
+export function CopyCodeBlock({ code, label }: CopyCodeBlockProps) {
+ const [copied, setCopied] = useState(false);
+
+ function handleCopy() {
+ navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+
+ return (
+
+
+
+ {label || "API Request"}
+
+
+
+
+ {code}
+
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/DatasetTable.tsx b/typescript-recipes/parallel-datacenter-map/src/components/DatasetTable.tsx
new file mode 100644
index 0000000..9c64409
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/DatasetTable.tsx
@@ -0,0 +1,589 @@
+"use client";
+
+import { useState, useMemo, useCallback } from "react";
+import clsx from "clsx";
+import type { Datacenter, DisplayStatus, Monitor, MonitorDetection } from "@/lib/types";
+import type { SnapshotUpdate } from "@/hooks/useMonitors";
+import {
+ STATUS_COLORS, STATUS_LABELS, STATE_TO_MONITOR, CA_SPLIT_LAT,
+ MONITOR_CATEGORY_LABELS, MONITOR_CATEGORY_COLORS, SEVERITY_COLORS,
+ AI_CLASS_LABELS, AI_CLASS_COLORS, IMPACT_LABELS, IMPACT_COLORS,
+ PUSHBACK_LABELS, PUSHBACK_COLORS,
+} from "@/lib/constants";
+import { toDisplayStatus, formatPower, formatSqft, timeAgo } from "@/lib/utils";
+import { ChevronUp, ChevronDown, X, RefreshCw } from "lucide-react";
+import { BasisPanel, type BasisPanelData } from "./BasisPanel";
+
+interface DatasetTableProps {
+ datacenters: Datacenter[];
+ monitors: Monitor[];
+ snapshotUpdates?: Record;
+}
+
+const PAGE_SIZE = 50;
+
+function getMonitorForDc(dc: Datacenter, monitors: Monitor[]): Monitor | null {
+ let monId = STATE_TO_MONITOR[dc.state];
+ if (dc.state === "CA" && dc.lat < CA_SPLIT_LAT) monId = "region-socal";
+ if (!monId) return null;
+ return monitors.find((m) => m.id === monId) || null;
+}
+
+/** Build severity strip colors from events */
+function getSeverityStrip(events: MonitorDetection[]): string[] {
+ const sevs = new Set(events.map((e) => e.severity));
+ const strip: string[] = [];
+ if (sevs.has("critical")) strip.push(SEVERITY_COLORS.critical);
+ if (sevs.has("notable")) strip.push(SEVERITY_COLORS.notable);
+ if (sevs.has("informational")) strip.push(SEVERITY_COLORS.informational);
+ return strip;
+}
+
+export function DatasetTable({ datacenters, monitors, snapshotUpdates = {} }: DatasetTableProps) {
+ const [sortField, setSortField] = useState("signals");
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
+ const [page, setPage] = useState(0);
+ const [activityWindow, setActivityWindow] = useState<"24h" | "7d" | "30d">("7d");
+ const [signalModal, setSignalModal] = useState<{
+ monitor: Monitor | null;
+ facilityName: string;
+ facilityIndex: number;
+ snapshot: SnapshotUpdate | null;
+ } | null>(null);
+ const [basisData, setBasisData] = useState(null);
+ const [hoveredDiff, setHoveredDiff] = useState<{ field: string; idx: number } | null>(null);
+ const [filters, setFilters] = useState({ name: "", state: "", ai: "", water: "", grid: "", community: "" });
+
+ function setFilter(key: keyof typeof filters, val: string) {
+ setFilters((f) => ({ ...f, [key]: val }));
+ setPage(0);
+ }
+ const anyFilterActive = Object.values(filters).some(Boolean);
+
+ const stateOptions = useMemo(
+ () => Array.from(new Set(datacenters.map((d) => d.state).filter(Boolean))).sort(),
+ [datacenters]
+ );
+
+ // Combinable in-table filters (AND together), applied on top of the toolbar scope/lifecycle
+ const visibleDatacenters = useMemo(() => {
+ const q = filters.name.toLowerCase().trim();
+ return datacenters.filter((dc) => {
+ if (q && !`${dc.name} ${dc.operator} ${dc.owner} ${dc.city}`.toLowerCase().includes(q)) return false;
+ if (filters.state && dc.state !== filters.state) return false;
+ if (filters.ai) {
+ if (filters.ai === "unclassified") { if (dc.aiClassification) return false; }
+ else if (dc.aiClassification?.ai_class !== filters.ai) return false;
+ }
+ if (filters.water && dc.aiClassification?.water_impact !== filters.water) return false;
+ if (filters.grid && dc.aiClassification?.grid_impact !== filters.grid) return false;
+ if (filters.community && dc.aiClassification?.community_pushback !== filters.community) return false;
+ return true;
+ });
+ }, [datacenters, filters]);
+
+ const openBasis = useCallback((dc: Datacenter, field: string, value: string, facilityIndex: number) => {
+ const e = dc.enrichment;
+ if (!e) return;
+ const snap = snapshotUpdates[String(facilityIndex)];
+ const hasUpdate = !!snap && snap.changedFields.includes(field);
+ const fieldBasis = hasUpdate ? snap!.basis?.[field] : undefined;
+ const update = hasUpdate
+ ? {
+ from: snap!.changes?.[field]?.from,
+ to: snap!.changes?.[field]?.to,
+ timestamp: snap!.timestamp,
+ reasoning: fieldBasis?.reasoning,
+ citations: fieldBasis?.citations?.map((c) => ({ field, url: c.url, title: c.title })),
+ }
+ : undefined;
+ setBasisData({
+ field, value: value || "Not found", facilityName: dc.name, facilityIndex,
+ citations: e.citations || [], reasoning: e.reasoning?.[field], source: "enrichment", update,
+ });
+ }, [snapshotUpdates]);
+
+ const openAiBasis = useCallback((dc: Datacenter, field: string, value: string, note: string, facilityIndex: number) => {
+ const ai = dc.aiClassification;
+ if (!ai) return;
+ setBasisData({
+ field, value: value || "Not found", facilityName: dc.name, facilityIndex,
+ citations: (ai.citations || []).map((c) => ({ field, url: c.url, title: c.title })),
+ reasoning: note, source: "ai",
+ });
+ }, []);
+
+ const enrichedRows = useMemo(() => {
+ return visibleDatacenters.map((dc, i) => {
+ const originalIndex = dc.sourceIndex ?? i;
+ const monitor = getMonitorForDc(dc, monitors);
+ const events = monitor?.events || [];
+ const snapshot = snapshotUpdates[String(originalIndex)];
+ const newestEvent = events.length > 0
+ ? events.reduce((a, b) => new Date(b.eventDate) > new Date(a.eventDate) ? b : a) : null;
+ const severityStrip = getSeverityStrip(events);
+ return { dc, monitor, events, newestEvent, originalIndex, snapshot, severityStrip };
+ });
+ }, [visibleDatacenters, monitors, snapshotUpdates]);
+
+ const sorted = useMemo(() => {
+ return [...enrichedRows].sort((a, b) => {
+ let cmp: number;
+ if (sortField === "signals") {
+ const aTotal = a.events.length + (a.snapshot ? 1 : 0);
+ const bTotal = b.events.length + (b.snapshot ? 1 : 0);
+ cmp = aTotal - bTotal;
+ if (cmp === 0 && a.newestEvent && b.newestEvent)
+ cmp = new Date(a.newestEvent.eventDate).getTime() - new Date(b.newestEvent.eventDate).getTime();
+ } else {
+ const aVal = a.dc[sortField as keyof Datacenter];
+ const bVal = b.dc[sortField as keyof Datacenter];
+ cmp = typeof aVal === "number" && typeof bVal === "number" ? aVal - bVal : String(aVal || "").localeCompare(String(bVal || ""));
+ }
+ return sortDir === "asc" ? cmp : -cmp;
+ });
+ }, [enrichedRows, sortField, sortDir]);
+
+ const totalPages = Math.ceil(sorted.length / PAGE_SIZE);
+ const pageData = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
+ const enrichedCount = datacenters.filter((d) => d.enrichment).length;
+
+ function handleSort(field: string) {
+ if (sortField === field) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
+ else { setSortField(field); setSortDir(field === "powerMw" || field === "sqft" || field === "signals" ? "desc" : "asc"); }
+ setPage(0);
+ }
+
+ return (
+
+
+ {/* Stats strip */}
+
+
+
s + d.powerMw, 0))} />
+ {enrichedCount > 0 && (
+
+ Task API
+ {enrichedCount.toLocaleString()} enriched
+
+ )}
+
+
+ {/* Filter bar — combinable cuts */}
+
+ Filter
+ setFilter("name", e.target.value)}
+ placeholder="Name / operator…"
+ className="font-mono text-[10px] text-[#181818] placeholder:text-[#B5B4B2] border border-[#E5E5E5] rounded-[3px] px-2 py-[3px] w-[150px] focus:outline-none focus:border-[#FB631B]"
+ />
+ setFilter("state", v)} placeholder="State" options={stateOptions.map((s) => ({ value: s, label: s }))} />
+ setFilter("ai", v)} placeholder="AI class" options={[
+ ...Object.entries(AI_CLASS_LABELS).map(([value, label]) => ({ value, label })),
+ { value: "unclassified", label: "Not classified" },
+ ]} />
+ setFilter("water", v)} placeholder="Water" options={Object.entries(IMPACT_LABELS).map(([value, label]) => ({ value, label }))} />
+ setFilter("grid", v)} placeholder="Grid" options={Object.entries(IMPACT_LABELS).map(([value, label]) => ({ value, label }))} />
+ setFilter("community", v)} placeholder="Community" options={Object.entries(PUSHBACK_LABELS).map(([value, label]) => ({ value, label }))} />
+ {anyFilterActive && (
+
+ )}
+ {visibleDatacenters.length.toLocaleString()} match{visibleDatacenters.length !== 1 ? "es" : ""}
+
+
+ {/* Legend bar */}
+
+
+
+ Cell value updated
+
+ {/* Activity window */}
+
+ Activity window
+ {(["24h", "7d", "30d"] as const).map((w) => (
+
+ ))}
+
+
+
+ {/* Scrollable table */}
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {pageData.map(({ dc, monitor, events, originalIndex, snapshot, severityStrip }, i) => {
+ const display = toDisplayStatus(dc.status);
+ const e = dc.enrichment;
+ const update = e?.recent_news || e?.construction_update || "";
+ const changedFields = snapshot?.changedFields || [];
+
+ return (
+
+ {/* Facility — sticky */}
+
+ |
+ |
+
+ {/* Monitors cell */}
+
+ {(() => {
+ const totalCount = events.length + (snapshot ? snapshot.changedFields.length : 0);
+ if (totalCount === 0) return —;
+ return (
+
+ );
+ })()}
+ |
+
+
+ |
+
+
+ |
+
+ {dc.state} |
+
+ | } />
+
+ {dc.type} |
+
+ {dc.aiClassification ? (
+
+
+ {AI_CLASS_LABELS[dc.aiClassification.ai_class]}
+
+
+ ) : —}
+
+
+ {dc.aiClassification ? (
+
+
+
+ ) : —}
+
+
+ {dc.aiClassification ? (
+
+
+
+ ) : —}
+
+
+ {dc.aiClassification ? (
+
+
+
+ ) : —}
+
+
+ 0 ? formatPower(dc.powerMw) : ""} onClick={openBasis} facilityIndex={originalIndex} />
+ |
+
+ 0 ? formatSqft(dc.sqft) : ""} onClick={openBasis} facilityIndex={originalIndex} />
+ |
+
+ |
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ 0 ? String(e.num_buildings) : ""} onClick={openBasis} facilityIndex={originalIndex} /> |
+ 0 ? String(e.campus_acres) : ""} onClick={openBasis} facilityIndex={originalIndex} /> |
+ |
+ |
+
+ );
+ })}
+
+
+
+
+ {/* Pagination */}
+
+
+ {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, sorted.length)} of {sorted.length.toLocaleString()}
+
+
+
+ {page + 1}/{totalPages}
+
+
+
+
+
+ {/* Basis side panel */}
+ {basisData && (
+
+ setBasisData(null)} />
+
+ )}
+
+ {/* Monitor detail modal */}
+ {signalModal && (
+
+
+
+
+
Monitors for facility
+
{signalModal.facilityName}
+
+
+
+
+ {signalModal.snapshot && (
+
+
+ Snapshot
+ Row-level monitor
+ {signalModal.snapshot.timestamp}
+
+
+ Daily snapshot detected changes in {signalModal.snapshot.changedFields.length} field{signalModal.snapshot.changedFields.length !== 1 ? "s" : ""}:
+
+
+ {signalModal.snapshot.changedFields.map((field) => (
+
+ {field.replace(/_/g, " ")}
+
+ ))}
+
+
+ )}
+ {signalModal.monitor && signalModal.monitor.events.length > 0 && (
+
+
+ Region
+ {signalModal.monitor.name}
+ {signalModal.monitor.events.length} event{signalModal.monitor.events.length !== 1 ? "s" : ""}
+
+
+ {signalModal.monitor.events.map((evt) => )}
+
+
+ )}
+ {!signalModal.snapshot && (!signalModal.monitor || signalModal.monitor.events.length === 0) && (
+
No monitor events for this facility yet.
+ )}
+
+
+
+ )}
+
+ );
+}
+
+/** Enriched cell with optional change indicator */
+function EC({ children, className, changed, field, snapshot, hoveredDiff, setHoveredDiff, idx }: {
+ children: React.ReactNode; className?: string; changed?: boolean; field?: string;
+ snapshot?: SnapshotUpdate | null; hoveredDiff?: { field: string; idx: number } | null;
+ setHoveredDiff?: (v: { field: string; idx: number } | null) => void; idx?: number;
+}) {
+ const isHovered = hoveredDiff?.field === field && hoveredDiff?.idx === idx;
+
+ return (
+ changed && field && setHoveredDiff?.({ field, idx: idx || 0 })}
+ onMouseLeave={() => changed && setHoveredDiff?.(null)}
+ >
+ {/* Orange left edge for changed cells */}
+ {changed && }
+
+ {children}
+ {changed && }
+
+ {/* Hover popover for changed cell */}
+ {isHovered && changed && field && snapshot && (
+
+ Snapshot · {field.replace(/_/g, " ")}
+
+ Re-verified {timeAgo(snapshot.timestamp)} by the daily snapshot monitor · Task API
+
+
+ )}
+ |
+ );
+}
+
+function Cell({ dc, field, value, onClick, className, displayValue, truncate: trunc, facilityIndex }: {
+ dc: Datacenter; field: string; value: string; onClick: (dc: Datacenter, field: string, value: string, idx: number) => void;
+ className?: string; displayValue?: React.ReactNode; truncate?: number; facilityIndex: number;
+}) {
+ const e = dc.enrichment;
+ const isEmpty = !value || value === "0" || value === "unknown";
+ const shown = trunc && value && value.length > trunc ? value.slice(0, trunc) + "..." : value;
+ if (!e) return {isEmpty ? "\u2014" : (displayValue || shown)};
+ return (
+
+ );
+}
+
+function FilterSelect({ value, onChange, placeholder, options }: {
+ value: string; onChange: (v: string) => void; placeholder: string; options: { value: string; label: string }[];
+}) {
+ return (
+
+ );
+}
+
+function ImpactDot({ color, label }: { color: string; label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+/** Clickable AI-classification cell — opens the basis panel with the model's evidence + citations. */
+function AiCell({ dc, field, value, note, facilityIndex, onClick, children }: {
+ dc: Datacenter; field: string; value: string; note: string; facilityIndex: number;
+ onClick: (dc: Datacenter, field: string, value: string, note: string, idx: number) => void;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+function EvtCard({ event }: { event: MonitorDetection }) {
+ const catColor = MONITOR_CATEGORY_COLORS[event.category] || "#858483";
+ return (
+
+
+ {MONITOR_CATEGORY_LABELS[event.category] || event.category}
+ {event.eventDate}
+
+
{event.headline}
+
{event.summary}
+
+ );
+}
+
+function TH({ f, l, s, d, o, a, e, sticky }: {
+ f: string; l: string; s: string; d: string; o: (f: string) => void; a?: "right"; e?: boolean; sticky?: boolean;
+}) {
+ return (
+ o(f)} className={clsx(
+ "px-4 py-2.5 font-mono font-medium text-[#858483] uppercase tracking-[0.05em] text-[8px] cursor-pointer hover:text-[#181818] select-none border-b border-[#E5E5E5] whitespace-nowrap",
+ a === "right" ? "text-right" : "text-left",
+ e && "border-l border-l-[#FCDDCF]/30",
+ sticky && "sticky left-0 bg-[#F6F6F6] z-[11] border-r border-[#E5E5E5]"
+ )}>
+
+ {l}{e && }
+ {s === f && (d === "asc" ? : )}
+
+ |
+ );
+}
+
+function THE({ l }: { l: string }) {
+ return (
+
+ {l}
+ |
+ );
+}
+
+function StatusBadge({ status }: { status: DisplayStatus }) {
+ return (
+
+
+ {STATUS_LABELS[status]}
+
+ );
+}
+
+function Stat({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/FilterPills.tsx b/typescript-recipes/parallel-datacenter-map/src/components/FilterPills.tsx
new file mode 100644
index 0000000..bbfc02c
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/FilterPills.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import clsx from "clsx";
+import type { DisplayStatus } from "@/lib/types";
+import { STATUS_LABELS } from "@/lib/constants";
+
+type FilterKey = DisplayStatus | "all";
+
+interface FilterPillsProps {
+ active: FilterKey;
+ counts: Record;
+ onChange: (filter: FilterKey) => void;
+}
+
+const FILTERS: FilterKey[] = [
+ "all",
+ "operational",
+ "construction",
+ "planned",
+ "decommissioned",
+];
+
+export function FilterPills({ active, counts, onChange }: FilterPillsProps) {
+ return (
+
+ {FILTERS.map((key) => {
+ const label =
+ key === "all" ? "All" : STATUS_LABELS[key as DisplayStatus];
+ const count = counts[key];
+ const isActive = active === key;
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/Header.tsx b/typescript-recipes/parallel-datacenter-map/src/components/Header.tsx
new file mode 100644
index 0000000..706b59d
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/Header.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { Mail } from "lucide-react";
+
+interface HeaderProps {
+ monitorCount: number;
+ detectedCount: number;
+ lastChecked: string;
+ onOpenBrief: () => void;
+}
+
+export function Header({
+ monitorCount,
+ detectedCount,
+ lastChecked,
+ onOpenBrief,
+}: HeaderProps) {
+ return (
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/MapLegend.tsx b/typescript-recipes/parallel-datacenter-map/src/components/MapLegend.tsx
new file mode 100644
index 0000000..dba3d65
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/MapLegend.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import type { DisplayStatus } from "@/lib/types";
+import { STATUS_COLORS, STATUS_LABELS } from "@/lib/constants";
+
+interface MapLegendProps {
+ counts: Record;
+}
+
+const STATUSES: DisplayStatus[] = [
+ "operational",
+ "construction",
+ "planned",
+ "decommissioned",
+];
+
+export function MapLegend({ counts }: MapLegendProps) {
+ return (
+
+
+ Datacenter lifecycle
+
+
+ {STATUSES.map((status) => (
+
+
+
+
+ {STATUS_LABELS[status]}
+
+
+
+ {counts[status]}
+
+
+ ))}
+
+
+ {counts.all.toLocaleString()} U.S. datacenter facilities.
+ Source: Task API. Continuously monitoring for site updates.
+
+
+ );
+}
+
+function StatusDot({ status }: { status: DisplayStatus }) {
+ const color = STATUS_COLORS[status];
+
+ if (status === "operational") {
+ return (
+
+ );
+ }
+
+ if (status === "construction") {
+ return (
+
+ );
+ }
+
+ if (status === "planned") {
+ return (
+
+ );
+ }
+
+ // unknown: hollow, lighter
+ return (
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/MapPanel.tsx b/typescript-recipes/parallel-datacenter-map/src/components/MapPanel.tsx
new file mode 100644
index 0000000..c3f5f0e
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/MapPanel.tsx
@@ -0,0 +1,468 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import {
+ MapContainer,
+ TileLayer,
+ CircleMarker,
+ Popup,
+ Tooltip,
+ useMap,
+} from "react-leaflet";
+import L from "leaflet";
+import "leaflet/dist/leaflet.css";
+import { ExternalLink, X } from "lucide-react";
+import type { Datacenter, DisplayStatus, Monitor } from "@/lib/types";
+import {
+ STATUS_COLORS,
+ STATUS_LABELS,
+ MAP_CENTER,
+ MAP_ZOOM,
+ TILE_URL,
+ TILE_ATTRIBUTION,
+ STATE_TO_MONITOR,
+ CA_SPLIT_LAT,
+ AI_CLASS_LABELS,
+ AI_CLASS_COLORS,
+ IMPACT_LABELS,
+ IMPACT_COLORS,
+ PUSHBACK_LABELS,
+ PUSHBACK_COLORS,
+} from "@/lib/constants";
+import { toDisplayStatus, formatPower, formatSqft } from "@/lib/utils";
+import { MapLegend } from "./MapLegend";
+
+interface MapPanelProps {
+ datacenters: Datacenter[];
+ counts: Record;
+ selectedMonitor: Monitor | null;
+ focusedLocation?: { lat: number; lng: number } | null;
+}
+
+/** Which monitor covers a given datacenter? */
+function getMonitorIdForDc(dc: Datacenter): string | null {
+ if (dc.state === "CA") {
+ return dc.lat < CA_SPLIT_LAT ? "region-socal" : "region-norcal";
+ }
+ return STATE_TO_MONITOR[dc.state] || null;
+}
+
+function getMarkerStyle(
+ status: DisplayStatus,
+ isHighlighted: boolean,
+ isDimmed: boolean
+) {
+ const base = {
+ operational: {
+ fillColor: STATUS_COLORS.operational,
+ fillOpacity: 1,
+ color: STATUS_COLORS.operational,
+ weight: 1.5,
+ radius: 4,
+ },
+ construction: {
+ fillColor: STATUS_COLORS.construction,
+ fillOpacity: 0.85,
+ color: STATUS_COLORS.construction,
+ weight: 1.5,
+ radius: 4,
+ },
+ planned: {
+ fillColor: STATUS_COLORS.planned,
+ fillOpacity: 0.3,
+ color: STATUS_COLORS.planned,
+ weight: 2,
+ radius: 4,
+ },
+ unknown: {
+ fillColor: "transparent",
+ fillOpacity: 0,
+ color: STATUS_COLORS.unknown,
+ weight: 1.5,
+ radius: 4,
+ },
+ decommissioned: {
+ fillColor: STATUS_COLORS.decommissioned,
+ fillOpacity: 0.6,
+ color: STATUS_COLORS.decommissioned,
+ weight: 2,
+ radius: 4,
+ },
+ }[status];
+
+ if (isHighlighted) {
+ return {
+ ...base,
+ fillColor: "#FB631B",
+ fillOpacity: 1,
+ color: "#FB631B",
+ weight: 2,
+ radius: 4,
+ };
+ }
+
+ if (isDimmed) {
+ return {
+ ...base,
+ fillOpacity: base.fillOpacity * 0.15,
+ opacity: 0.15,
+ radius: 2,
+ };
+ }
+
+ return base;
+}
+
+/** Flies the map to fit highlighted facilities */
+function FlyToSelection({
+ datacenters,
+ selectedMonitor,
+}: {
+ datacenters: Datacenter[];
+ selectedMonitor: Monitor | null;
+}) {
+ const map = useMap();
+
+ useEffect(() => {
+ if (!selectedMonitor) {
+ // Reset to default view
+ map.flyTo(MAP_CENTER, MAP_ZOOM, { duration: 0.8 });
+ return;
+ }
+
+ // Find matching facilities
+ const matching = datacenters.filter((dc) => {
+ if (selectedMonitor.class === "facility") {
+ return (
+ dc.operator?.toLowerCase().includes("qts") &&
+ selectedMonitor.states?.includes(dc.state)
+ );
+ }
+ if (selectedMonitor.states?.length) {
+ const monId = getMonitorIdForDc(dc);
+ return monId === selectedMonitor.id;
+ }
+ return false;
+ });
+
+ if (matching.length === 0) return;
+
+ // Compute bounds
+ const bounds = L.latLngBounds(matching.map((dc) => [dc.lat, dc.lng]));
+ map.flyToBounds(bounds.pad(0.4), { duration: 0.8, maxZoom: 8 });
+ }, [selectedMonitor, datacenters, map]);
+
+ return null;
+}
+
+/** Flies to a specific location when triggered */
+function FlyToLocation({ location }: { location: { lat: number; lng: number } | null | undefined }) {
+ const map = useMap();
+ useEffect(() => {
+ if (location) map.flyTo([location.lat, location.lng], 7, { duration: 0.8 });
+ }, [location, map]);
+ return null;
+}
+
+export default function MapPanel({
+ datacenters,
+ counts,
+ selectedMonitor,
+ focusedLocation,
+}: MapPanelProps) {
+ // Precompute which DCs are highlighted
+ const highlightSet = useMemo(() => {
+ if (!selectedMonitor) return null;
+
+ const set = new Set();
+ datacenters.forEach((dc, i) => {
+ if (selectedMonitor.class === "facility") {
+ if (
+ dc.operator?.toLowerCase().includes("qts") &&
+ selectedMonitor.states?.includes(dc.state)
+ ) {
+ set.add(i);
+ }
+ } else if (selectedMonitor.states?.length) {
+ const monId = getMonitorIdForDc(dc);
+ if (monId === selectedMonitor.id) {
+ set.add(i);
+ }
+ }
+ });
+ return set;
+ }, [selectedMonitor, datacenters]);
+
+ return (
+
+
+
+
+
+ {datacenters.map((dc, i) => {
+ const display = toDisplayStatus(dc.status);
+ const isHighlighted = highlightSet?.has(i) ?? false;
+ const isDimmed = highlightSet !== null && !isHighlighted;
+ const style = getMarkerStyle(display, isHighlighted, isDimmed);
+
+ return (
+
+
+
+
{dc.name}
+
{dc.operator} · {dc.city}, {dc.state}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+function ImpactChip({
+ label,
+ color,
+ value,
+}: {
+ label: string;
+ color: string;
+ value: string;
+}) {
+ return (
+
+
+ {label} {value}
+
+ );
+}
+
+function FacilityPopup({
+ dc,
+ display,
+}: {
+ dc: Datacenter;
+ display: DisplayStatus;
+}) {
+ const map = useMap();
+ const e = dc.enrichment;
+
+ // Citations are stripped from the static bundle and loaded on demand.
+ // This component only mounts when the popup actually opens (react-leaflet
+ // portals children lazily), so the fetch fires once per open.
+ const [citations, setCitations] = useState<{ url: string; title: string }[]>([]);
+ const [loadingCites, setLoadingCites] = useState(false);
+
+ useEffect(() => {
+ if (dc.sourceIndex == null) return;
+ let cancelled = false;
+ setLoadingCites(true);
+ fetch(`/api/basis?facility=${dc.sourceIndex}`)
+ .then((r) => r.json())
+ .then((d: { citations?: { url: string; title: string }[] }) => {
+ if (cancelled) return;
+ const list = (d.citations || []).filter((c) => c.url?.startsWith("http"));
+ const unique = Array.from(new Map(list.map((c) => [c.url, c])).values());
+ setCitations(unique.slice(0, 6));
+ })
+ .catch(() => {})
+ .finally(() => { if (!cancelled) setLoadingCites(false); });
+ return () => { cancelled = true; };
+ }, [dc.sourceIndex]);
+
+ return (
+
+ {/* Header — fixed above the scroll area so the close button is always reachable */}
+
+
+ {dc.name}
+
+
+
+ {STATUS_LABELS[display]}
+
+
+
+
+
+ {/* Scrollable body — keeps a long enriched popup from overflowing the map */}
+
+ {/* Enriched description */}
+ {e?.description && (
+
+ {e.description}
+
+ )}
+
+ {/* Core fields */}
+
+ {dc.operator && (
+
+ Operator: {dc.operator}
+
+ )}
+ {dc.owner && dc.owner !== dc.operator && (
+
+ Owner: {dc.owner}
+
+ )}
+
+ Location: {dc.city},{" "}
+ {dc.state}
+
+
+ Type: {dc.type}
+
+ {dc.powerMw > 0 && (
+
+ Power:{" "}
+ {formatPower(dc.powerMw)}
+
+ )}
+ {dc.sqft > 0 && (
+
+ Size:{" "}
+ {formatSqft(dc.sqft)}
+
+ )}
+ {dc.yearOnline && dc.yearOnline !== "unknown" && (
+
+ Online:{" "}
+ {dc.yearOnline}
+
+ )}
+ {e?.notable_tenants && (
+
+ Tenants:{" "}
+ {e.notable_tenants}
+
+ )}
+
+
+ {/* AI classification (Task API) — compact; full analysis lives in the dataset view */}
+ {dc.aiClassification && (
+
+
+
+ AI classification
+
+
+ {AI_CLASS_LABELS[dc.aiClassification.ai_class]}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Construction update */}
+ {e?.construction_update && (
+
+
+ Construction update
+
+
+ {e.construction_update}
+
+
+ )}
+
+ {/* Recent news */}
+ {e?.recent_news && (
+
+
+ Recent news
+
+
+ {e.recent_news}
+
+
+ )}
+
+ {/* Citations (loaded on demand from Task API basis) */}
+ {(loadingCites || citations.length > 0) && (
+
+
+ {loadingCites && citations.length === 0
+ ? "Loading sources…"
+ : `Sources (${citations.length})`}
+
+
+
+ )}
+
+ {/* Enrichment badge */}
+ {e && (
+
+
+ Enriched by Task API
+ {e.enrichedAt &&
+ ` · ${new Date(e.enrichedAt).toLocaleDateString()}`}
+
+
+ )}
+
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/MonitorCard.tsx b/typescript-recipes/parallel-datacenter-map/src/components/MonitorCard.tsx
new file mode 100644
index 0000000..f85f7e0
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/MonitorCard.tsx
@@ -0,0 +1,235 @@
+"use client";
+
+import { useState } from "react";
+import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
+import type { Monitor, MonitorDetection } from "@/lib/types";
+import { CopyCodeBlock } from "./CopyCodeBlock";
+import {
+ MONITOR_CATEGORY_LABELS,
+ MONITOR_CATEGORY_COLORS,
+ SEVERITY_COLORS,
+} from "@/lib/constants";
+
+interface MonitorCardProps {
+ monitor: Monitor;
+ isSelected: boolean;
+ onSelect: () => void;
+}
+
+export function MonitorCard({ monitor, isSelected, onSelect }: MonitorCardProps) {
+ const [expanded, setExpanded] = useState(false);
+ const [showQuery, setShowQuery] = useState(false);
+ const hasEvents = monitor.events.length > 0;
+
+ const classBg =
+ monitor.class === "region"
+ ? "bg-[#F6F6F6] text-[#5C5B59]"
+ : monitor.class === "facility"
+ ? "bg-[#FCDDCF] text-[#FB631B]"
+ : "bg-[#1D1B16] text-white";
+
+ return (
+
+ {/* Monitor header — always visible */}
+
+
+ {/* Expanded content */}
+ {expanded && (
+
+ {/* Query text */}
+
+
+ {showQuery && (
+
+ {monitor.query}
+
+ )}
+
+
+ {/* Events */}
+ {hasEvents ? (
+
+ {monitor.events.map((evt) => (
+
+ ))}
+
+ ) : (
+
+ Monitor is active and checking every {monitor.frequency}. Events
+ will appear here when signals are detected.
+
+ )}
+
+ )}
+
+ );
+}
+
+function DetectionCard({ detection, monitor }: { detection: MonitorDetection; monitor: Monitor }) {
+ const [showPayload, setShowPayload] = useState(false);
+ const catLabel =
+ MONITOR_CATEGORY_LABELS[detection.category] || detection.category;
+ const catColor =
+ MONITOR_CATEGORY_COLORS[detection.category] || "#858483";
+ const sevColor = SEVERITY_COLORS[detection.severity] || "#858483";
+
+ const validCitations = detection.citations.filter(
+ (c) => c.url && c.url.startsWith("http")
+ );
+
+ return (
+
+ {/* Category + severity + date */}
+
+
+ {catLabel}
+
+
+ {detection.severity}
+
+
+ {detection.eventDate}
+
+
+
+ {/* Headline */}
+
+ {detection.headline}
+
+
+ {/* Summary */}
+
+ {detection.summary}
+
+
+ {/* Affected entities */}
+ {detection.affectedEntities && (
+
+ Affects: {detection.affectedEntities}
+
+ )}
+
+ {/* Citations + payload toggle */}
+
+
+ {/* API request */}
+ {showPayload && (
+
+
+
+ )}
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/MonitorPanel.tsx b/typescript-recipes/parallel-datacenter-map/src/components/MonitorPanel.tsx
new file mode 100644
index 0000000..6489e37
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/MonitorPanel.tsx
@@ -0,0 +1,257 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import clsx from "clsx";
+import { ExternalLink, Crosshair, Mail } from "lucide-react";
+import type { Monitor, MonitorDetection } from "@/lib/types";
+import {
+ MONITOR_CATEGORY_LABELS,
+ MONITOR_CATEGORY_COLORS,
+ SEVERITY_COLORS,
+ REGION_CENTROIDS,
+} from "@/lib/constants";
+import { relativeDate } from "@/lib/utils";
+
+type BreakdownDim = "time" | "category" | "severity";
+
+interface MonitorPanelProps {
+ monitors: Monitor[];
+ selectedMonitorId: string | null;
+ onSelectMonitor: (monitor: Monitor | null) => void;
+ onLocateEvent?: (lat: number, lng: number) => void;
+ onOpenBrief?: () => void;
+}
+
+export function MonitorPanel({
+ monitors,
+ selectedMonitorId,
+ onSelectMonitor,
+ onLocateEvent,
+ onOpenBrief,
+}: MonitorPanelProps) {
+ const [breakdown, setBreakdown] = useState("category");
+ const [filterBucket, setFilterBucket] = useState(null);
+
+ // Flatten all events
+ const allEvents = useMemo(() => {
+ const events: { event: MonitorDetection; monitor: Monitor }[] = [];
+ for (const m of monitors) {
+ for (const e of m.events) events.push({ event: e, monitor: m });
+ }
+ events.sort((a, b) => new Date(b.event.eventDate).getTime() - new Date(a.event.eventDate).getTime());
+ return events;
+ }, [monitors]);
+
+ const totalEvents = allEvents.length;
+ const criticalCount = allEvents.filter((e) => e.event.severity === "critical").length;
+
+ // Bucket events by the current breakdown dimension
+ const buckets = useMemo(() => {
+ const map: Record = {};
+ for (const { event } of allEvents) {
+ let key: string;
+ let color = "#FB631B";
+ if (breakdown === "category") {
+ key = event.category;
+ color = MONITOR_CATEGORY_COLORS[event.category] || "#FB631B";
+ } else if (breakdown === "severity") {
+ key = event.severity;
+ color = SEVERITY_COLORS[event.severity] || "#858483";
+ } else {
+ const d = new Date(event.eventDate);
+ const weekStart = new Date(d);
+ weekStart.setDate(d.getDate() - d.getDay());
+ key = weekStart.toISOString().slice(0, 10);
+ }
+ if (!map[key]) map[key] = { total: 0, critical: 0, color };
+ map[key].total++;
+ if (event.severity === "critical") map[key].critical++;
+ }
+ return Object.entries(map)
+ .map(([key, v]) => ({ key, ...v }))
+ .sort((a, b) => breakdown === "time" ? a.key.localeCompare(b.key) : b.total - a.total);
+ }, [allEvents, breakdown]);
+
+ // Filtered events
+ const filteredEvents = useMemo(() => {
+ if (!filterBucket) return allEvents;
+ return allEvents.filter(({ event }) => {
+ if (breakdown === "category") return event.category === filterBucket;
+ if (breakdown === "severity") return event.severity === filterBucket;
+ const d = new Date(event.eventDate);
+ const weekStart = new Date(d);
+ weekStart.setDate(d.getDate() - d.getDay());
+ return weekStart.toISOString().slice(0, 10) === filterBucket;
+ });
+ }, [allEvents, filterBucket, breakdown]);
+
+ const maxBucket = Math.max(...buckets.map((b) => b.total), 1);
+ const filterLabel = filterBucket
+ ? breakdown === "category" ? (MONITOR_CATEGORY_LABELS[filterBucket as keyof typeof MONITOR_CATEGORY_LABELS] || filterBucket) : filterBucket
+ : null;
+
+ function handleLocate(monitor: Monitor) {
+ const centroid = REGION_CENTROIDS[monitor.id];
+ if (centroid && onLocateEvent) onLocateEvent(centroid[0], centroid[1]);
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ MONITORS
+
+
+ {monitors.length} active · {totalEvents} detected
+
+
+
+ {totalEvents} events this week
+ {criticalCount > 0 && <> · {criticalCount} critical>}
+
+
+
+
+ {/* Pivot chart block */}
+
+
+
Break down by
+
+ {(["time", "category", "severity"] as const).map((dim) => (
+
+ ))}
+
+
+
+ {breakdown === "time" ? (
+
+
+ {buckets.map((b) => {
+ const totalH = (b.total / maxBucket) * 100;
+ const critH = (b.critical / maxBucket) * 100;
+ const restH = totalH - critH;
+ const isActive = filterBucket === b.key;
+ return (
+
setFilterBucket(filterBucket === b.key ? null : b.key)}
+ className={clsx("flex-1 flex flex-col justify-end h-full cursor-pointer rounded-t-[2px] transition-opacity", !isActive && filterBucket ? "opacity-40" : "")}>
+ {critH > 0 && }
+
+
+ );
+ })}
+
+
+ {buckets.length > 0 && {buckets[0].key.slice(5)}}
+ {buckets.length > 1 && {buckets[buckets.length - 1].key.slice(5)}}
+
+
+ ) : (
+
+ {buckets.slice(0, 12).map((b, i) => {
+ const pct = (b.total / maxBucket) * 100;
+ const isActive = filterBucket === b.key;
+ const label = breakdown === "category" ? (MONITOR_CATEGORY_LABELS[b.key as keyof typeof MONITOR_CATEGORY_LABELS] || b.key) : b.key;
+ return (
+
setFilterBucket(filterBucket === b.key ? null : b.key)}
+ className={clsx("flex items-center gap-[8px] py-[3px] px-[6px] rounded-[2px] cursor-pointer transition-colors",
+ isActive ? "bg-[#FCDDCF55]" : "hover:bg-[#FAF8F4]", i === 0 && !filterBucket ? "bg-[#FCDDCF55]" : ""
+ )} style={isActive ? { borderLeft: "2px solid #FB631B", paddingLeft: 4 } : {}}>
+
{label}
+
+
{b.total}
+
+ );
+ })}
+
+ )}
+
+
+
+ All events
+ Critical
+
+
Click a bar to filter ↓
+
+
+
+ {/* Active filter */}
+ {filterBucket && (
+
+ Filtered to
+ setFilterBucket(null)}>
+ {filterLabel} ×
+
+ {filteredEvents.length} event{filteredEvents.length !== 1 ? "s" : ""}
+
+ )}
+
+ {/* Feed */}
+
+
+ {filterBucket ? `${filterLabel} · ${filteredEvents.length} event${filteredEvents.length !== 1 ? "s" : ""}` : "All events · newest first"}
+
+
+ {filteredEvents.slice(0, 20).map(({ event, monitor }) => {
+ const validCitations = event.citations.filter((c) => c.url?.startsWith("http"));
+ return (
+
+
+
+
+
+
+ {MONITOR_CATEGORY_LABELS[event.category] || event.category}
+
+ {monitor.name}
+
+
{relativeDate(event.eventDate)}
+
+
{event.headline}
+ {event.severity === "critical" && (
+
+ )}
+
{event.summary}
+ {event.affectedEntities && (
+
Affects: {event.affectedEntities}
+ )}
+
+
+
+
+ );
+ })}
+ {filteredEvents.length > 20 && (
+
+ View all {filteredEvents.length} {filterLabel || ""} events →
+
+ )}
+
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/NewsletterIssue.tsx b/typescript-recipes/parallel-datacenter-map/src/components/NewsletterIssue.tsx
new file mode 100644
index 0000000..7df663c
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/NewsletterIssue.tsx
@@ -0,0 +1,268 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+import { X, Loader2 } from "lucide-react";
+
+interface NewsletterIssueProps {
+ onClose: () => void;
+}
+
+interface IssueMeta {
+ issueNumber: number;
+ weekOf: string;
+ hasContent: boolean;
+ focus?: string;
+ generatedAt?: string;
+ stats?: { events?: number; critical?: number; markets?: number };
+ isCurrent: boolean;
+}
+
+type ReadStatus = "loading" | "ready" | "generating" | "not_found";
+
+/** Strip email-only chrome that may linger in older stored issue bodies. */
+function sanitize(html: string): string {
+ return html
+ .replace(/\{\{UNSUBSCRIBE_URL\}\}/g, "#")
+ .replace(/]*>\s*Unsubscribe\s*<\/a>/gi, "");
+}
+
+export function NewsletterIssue({ onClose }: NewsletterIssueProps) {
+ const [issues, setIssues] = useState([]);
+ const [currentIssue, setCurrentIssue] = useState(0);
+ const [selected, setSelected] = useState(null);
+ const [status, setStatus] = useState("loading");
+ const [contentByIssue, setContentByIssue] = useState>({});
+ const [metaByIssue, setMetaByIssue] = useState>({});
+ const pollRef = useRef | null>(null);
+
+ const selectedMeta = selected != null ? metaByIssue[selected] : undefined;
+
+ function clearPoll() {
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
+ }
+
+ async function loadIssue(n: number) {
+ setSelected(n);
+ clearPoll();
+ if (contentByIssue[n]) { setStatus("ready"); return; }
+ setStatus("loading");
+ try {
+ const res = await fetch(`/api/newsletter/preview?issue=${n}`);
+ const data = await res.json();
+ if (data.status === "found" && data.content) {
+ setContentByIssue((p) => ({ ...p, [n]: sanitize(data.content) }));
+ setStatus("ready");
+ } else if (data.status === "generating") {
+ setStatus("generating");
+ startPolling(n);
+ } else {
+ setStatus("not_found");
+ }
+ } catch {
+ setStatus("not_found");
+ }
+ }
+
+ function startPolling(n: number) {
+ clearPoll();
+ pollRef.current = setInterval(async () => {
+ try {
+ const res = await fetch(`/api/newsletter/preview?issue=${n}`);
+ const data = await res.json();
+ if (data.status === "found" && data.content) {
+ clearPoll();
+ setContentByIssue((p) => ({ ...p, [n]: sanitize(data.content) }));
+ setMetaByIssue((p) => ({ ...p, [n]: { ...(p[n] || {} as IssueMeta), hasContent: true } }));
+ setStatus("ready");
+ }
+ } catch {}
+ }, 10000);
+ setTimeout(clearPoll, 360000);
+ }
+
+ async function handleGenerate(n: number) {
+ setStatus("generating");
+ fetch("/api/newsletter/generate", { method: "POST" }).catch(() => {});
+ startPolling(n);
+ }
+
+ // Load the archive on mount
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const res = await fetch("/api/newsletter/issues");
+ const data = await res.json();
+ if (cancelled) return;
+ const list: IssueMeta[] = data.issues || [];
+ const cur: number = data.currentIssue || 0;
+ setCurrentIssue(cur);
+
+ // Ensure the current week is always represented, even if not yet generated
+ const merged = [...list];
+ if (cur && !merged.some((i) => i.issueNumber === cur)) {
+ const ms = new Date("2024-01-01").getTime() + cur * 7 * 24 * 60 * 60 * 1000;
+ merged.unshift({
+ issueNumber: cur,
+ weekOf: new Date(ms).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
+ hasContent: false,
+ isCurrent: true,
+ });
+ }
+ merged.sort((a, b) => b.issueNumber - a.issueNumber);
+ setIssues(merged);
+ setMetaByIssue(Object.fromEntries(merged.map((i) => [i.issueNumber, i])));
+
+ const first = merged.find((i) => i.hasContent) || merged[0];
+ if (first) loadIssue(first.issueNumber);
+ else setStatus("not_found");
+ } catch {
+ if (!cancelled) setStatus("not_found");
+ }
+ })();
+ return () => { cancelled = true; clearPoll(); };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const content = selected != null ? contentByIssue[selected] : "";
+
+ return (
+
+
+ {/* Masthead */}
+
+
+ parallel
+ Weekly newsletter
+
+
+
+
+ Built with Task API
+
+
+
+
+
+
+ {/* Archive rail */}
+
+
+ {/* Reader pane */}
+
+ {/* Provenance strip */}
+ {selectedMeta && (
+
+
+ Deep-researched and written by Parallel Task API across 31 monitors — every claim links to a source.
+
+ {selectedMeta.stats?.events != null && (
+
+ {selectedMeta.stats.events} events · {selectedMeta.stats.markets ?? "—"} markets analyzed
+
+ )}
+
+ )}
+
+ {/* Body */}
+
+ {status === "loading" && (
+
+
+ Loading issue…
+
+ )}
+
+ {status === "generating" && (
+
+
+ Generating brief…
+
+ Parallel's Task API is deep-researching this week's critical events across all 31 monitors. Takes 3–5 minutes.
+
+
+ )}
+
+ {status === "not_found" && (
+
+
+ {selected === currentIssue ? "This week's issue isn't out yet" : "No issue here"}
+
+
+ Generate it now from your live monitor events — Parallel's Task API deep-researches each critical development and writes the brief.
+
+
+
Takes 3–5 minutes
+
+ )}
+
+ {status === "ready" && content && (
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Datacenter Signal · Published weekly · Powered by Parallel Task API
+
+ hello@parallel.ai
+
+
+
+
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/components/Toolbar.tsx b/typescript-recipes/parallel-datacenter-map/src/components/Toolbar.tsx
new file mode 100644
index 0000000..2760c1d
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/components/Toolbar.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import clsx from "clsx";
+import type { DisplayStatus } from "@/lib/types";
+import { FilterPills } from "./FilterPills";
+
+type Tab = "map" | "dataset";
+type FilterKey = DisplayStatus | "all";
+
+interface ToolbarProps {
+ activeTab: Tab;
+ onTabChange: (tab: Tab) => void;
+ activeFilter: FilterKey;
+ counts: Record;
+ onFilterChange: (filter: FilterKey) => void;
+ trackedCount: number;
+ aiCount: number;
+ totalCount: number;
+ aiOnly: boolean;
+ onAiOnlyChange: (aiOnly: boolean) => void;
+}
+
+export function Toolbar({
+ activeTab,
+ onTabChange,
+ activeFilter,
+ counts,
+ onFilterChange,
+ trackedCount,
+ aiCount,
+ totalCount,
+ aiOnly,
+ onAiOnlyChange,
+}: ToolbarProps) {
+ return (
+
+
+
+ {(["map", "dataset"] as const).map((tab) => (
+
+ ))}
+
+
+ {trackedCount.toLocaleString()} shown
+
+
+
+
+ {/* Scope: All facilities vs AI datacenters (mutually exclusive) */}
+ {aiCount > 0 && (
+
+
Scope
+
+ onAiOnlyChange(false)} label="All facilities" count={totalCount} />
+ onAiOnlyChange(true)} label="AI datacenters" count={aiCount} accent />
+
+
+ )}
+
+
+
+ {/* Lifecycle status (counts reflect current scope) */}
+
+ Lifecycle
+
+
+
+
+ );
+}
+
+function ScopeButton({ active, onClick, label, count, accent }: {
+ active: boolean; onClick: () => void; label: string; count: number; accent?: boolean;
+}) {
+ return (
+
+ );
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/data/datacenters.ts b/typescript-recipes/parallel-datacenter-map/src/data/datacenters.ts
new file mode 100644
index 0000000..78967ac
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/data/datacenters.ts
@@ -0,0 +1,114 @@
+import type { AiClassification, Datacenter } from "@/lib/types";
+import rawData from "../../public/data/datacenters.json";
+import compactData from "../../public/data/enrichments-compact.json";
+import aiData from "../../public/data/ai-classifications.json";
+
+const compactMap = compactData as Record>;
+const aiMap = aiData as Record;
+
+// Merge enrichment + AI classification onto each raw row, stamping the original
+// index (the key into all index-addressed data: compact/AI maps, snapshot
+// monitors, and per-facility blob files).
+const enriched: Datacenter[] = (rawData as Datacenter[]).map((raw, i) => {
+ const ai = aiMap[String(i)];
+ const base: Datacenter = { ...raw, sourceIndex: i, ...(ai ? { aiClassification: ai } : {}) };
+
+ const e = compactMap[String(i)];
+ if (!e) return base;
+
+ const name =
+ (e.verified_name as string) && (e.verified_name as string) !== raw.name
+ ? (e.verified_name as string)
+ : raw.name;
+ const operator =
+ (e.verified_operator as string) && (e.verified_operator as string) !== raw.operator
+ ? (e.verified_operator as string)
+ : raw.operator;
+ const owner = (e.verified_owner as string) || raw.owner;
+
+ return {
+ ...base,
+ name,
+ operator,
+ owner,
+ powerMw: (e.power_capacity_mw as number) > 0 ? (e.power_capacity_mw as number) : raw.powerMw,
+ sqft: (e.total_sqft as number) > 0 ? (e.total_sqft as number) : raw.sqft,
+ yearOnline: (e.year_online as string) && (e.year_online as string) !== "unknown" ? (e.year_online as string) : raw.yearOnline,
+ status: (e.verified_status as string) ? (e.verified_status as Datacenter["status"]) : raw.status,
+ enrichment: {
+ description: (e.description as string) || "",
+ verified_status: (e.verified_status as string) || "",
+ power_capacity_mw: (e.power_capacity_mw as number) || 0,
+ total_sqft: (e.total_sqft as number) || 0,
+ year_online: (e.year_online as string) || "",
+ construction_update: (e.construction_update as string) || "",
+ recent_news: (e.recent_news as string) || "",
+ notable_tenants: (e.notable_tenants as string) || "",
+ verified_name: (e.verified_name as string) || "",
+ verified_operator: (e.verified_operator as string) || "",
+ verified_owner: (e.verified_owner as string) || "",
+ cooling_type: (e.cooling_type as string) || "",
+ tier_level: (e.tier_level as string) || "",
+ fiber_providers: (e.fiber_providers as string) || "",
+ num_buildings: (e.num_buildings as number) || 0,
+ campus_acres: (e.campus_acres as number) || 0,
+ utility_provider: (e.utility_provider as string) || "",
+ tax_incentives: (e.tax_incentives as string) || "",
+ natural_hazard_zone: (e.natural_hazard_zone as string) || "",
+ // Citations/reasoning loaded on demand from blob
+ citations: [],
+ reasoning: {},
+ enrichedAt: "",
+ runId: "",
+ },
+ };
+});
+
+// ─── Dedup ──────────────────────────────────────────────────────────────
+// The source dataset (merged from two CSVs) contains duplicate rows for the
+// same physical facility. Collapse them so each facility appears once. We key
+// on the RAW name (before verified_name overwrites it) plus rounded location,
+// which catches true duplicates without merging distinct buildings on one
+// campus (their names carry different building numbers/letters that survive
+// normalization, e.g. "PHX-07" vs "PHX-13").
+
+function normName(n: string): string {
+ return (n || "")
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, " ")
+ .replace(/\b(data|center|centre|campus|project|the|llc|inc|building)\b/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+/** Higher = keep. Prefers richer enrichment; penalizes divergent "dead" statuses. */
+function score(dc: Datacenter): number {
+ let s = 0;
+ const e = dc.enrichment;
+ if (e) {
+ s += [
+ e.description, e.notable_tenants, e.recent_news, e.construction_update,
+ e.utility_provider, e.cooling_type, e.tier_level, e.fiber_providers,
+ e.tax_incentives, e.natural_hazard_zone,
+ ].filter(Boolean).length;
+ if (e.power_capacity_mw > 0) s++;
+ if (e.total_sqft > 0) s++;
+ }
+ if (dc.aiClassification) s += 3;
+ if (dc.status === "decommissioned") s -= 5;
+ if (dc.status === "unknown") s -= 3;
+ return s;
+}
+
+const groups = new Map();
+(rawData as Datacenter[]).forEach((raw, i) => {
+ const key = `${normName(raw.name)}|${raw.lat.toFixed(3)},${raw.lng.toFixed(3)}`;
+ const dc = enriched[i];
+ const g = groups.get(key);
+ if (g) g.push(dc);
+ else groups.set(key, [dc]);
+});
+
+export const datacenters: Datacenter[] = Array.from(groups.values())
+ .map((g) => (g.length === 1 ? g[0] : g.slice().sort((a, b) => score(b) - score(a))[0]))
+ .sort((a, b) => (a.sourceIndex ?? 0) - (b.sourceIndex ?? 0));
diff --git a/typescript-recipes/parallel-datacenter-map/src/data/monitors.json b/typescript-recipes/parallel-datacenter-map/src/data/monitors.json
new file mode 100644
index 0000000..d97354d
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/data/monitors.json
@@ -0,0 +1,356 @@
+{
+ "region-nova": {
+ "monitorId": "",
+ "name": "Northern Virginia",
+ "class": "region",
+ "query": "Data center developments in Loudoun and Prince William County, Virginia: Dominion Energy interconnection and large-load filings, SCC transmission approvals, county zoning and special-exception votes. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Northern Virginia",
+ "states": [
+ "VA"
+ ]
+ },
+ "region-atlanta": {
+ "monitorId": "",
+ "name": "Atlanta / Georgia",
+ "class": "region",
+ "query": "Data center developments in Georgia (metro Atlanta, Fayette, Coweta, DeKalb counties): Georgia Power interconnection actions, county rezoning approvals and denials, development moratoria. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Atlanta, GA",
+ "states": [
+ "GA"
+ ]
+ },
+ "region-ohio": {
+ "monitorId": "",
+ "name": "Central Ohio",
+ "class": "region",
+ "query": "Data center developments in central Ohio (New Albany, Columbus, Licking County, Etna Township): AEP Ohio power agreements and tariff disputes, township zoning bans, statewide moratorium legislation. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Central Ohio",
+ "states": [
+ "OH"
+ ]
+ },
+ "region-phoenix": {
+ "monitorId": "",
+ "name": "Phoenix / Arizona",
+ "class": "region",
+ "query": "Data center developments in metro Phoenix and Maricopa County, Arizona: APS and SRP power capacity, groundwater restrictions, municipal water-use caps for data centers, Pinal County developments. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Phoenix, AZ",
+ "states": [
+ "AZ"
+ ]
+ },
+ "region-utah": {
+ "monitorId": "",
+ "name": "Utah",
+ "class": "region",
+ "query": "Data center developments in Utah (Eagle Mountain, Salt Lake City corridor, West Jordan): Rocky Mountain Power load commitments, municipal water and power approvals, West Valley and Iron County permits. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Utah",
+ "states": [
+ "UT"
+ ]
+ },
+ "region-texas": {
+ "monitorId": "",
+ "name": "Texas",
+ "class": "region",
+ "query": "Data center developments in Texas: ERCOT large-load interconnection rules and Batch Zero process, grid-reliability legislation, PUCT actions, municipal approvals in Dallas, San Antonio, and Red Oak. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Texas",
+ "states": [
+ "TX"
+ ]
+ },
+ "region-pnw": {
+ "monitorId": "",
+ "name": "Pacific Northwest",
+ "class": "region",
+ "query": "Data center developments in Washington and Oregon (Seattle, Hillsboro, Quincy, The Dalles): Seattle municipal moratoria, BPA and PSE power constraints, Bonneville Power Administration capacity, Oregon permitting. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Pacific Northwest",
+ "states": [
+ "WA",
+ "OR"
+ ]
+ },
+ "region-florida": {
+ "monitorId": "",
+ "name": "Florida",
+ "class": "region",
+ "query": "Data center developments in Florida: county moratoria and rezoning freezes (Citrus, Palm Coast, Flagler, Lake), FPL power agreements, hurricane resilience requirements. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Florida",
+ "states": [
+ "FL"
+ ]
+ },
+ "region-norcal": {
+ "monitorId": "",
+ "name": "Northern California",
+ "class": "region",
+ "query": "Data center developments in Northern California (Silicon Valley, Santa Clara, Sacramento, Stockton): PG&E power capacity and interconnection, CPUC filings, Santa Clara city approvals, Bay Area environmental review. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Northern California",
+ "states": [
+ "CA"
+ ]
+ },
+ "region-socal": {
+ "monitorId": "",
+ "name": "Southern California",
+ "class": "region",
+ "query": "Data center developments in Southern California (Los Angeles, Inland Empire, San Diego): SCE and LADWP power capacity, SCAQMD air quality permits, water restrictions, Riverside and San Bernardino county approvals. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Southern California",
+ "states": [
+ "CA"
+ ]
+ },
+ "region-chicago": {
+ "monitorId": "",
+ "name": "Chicago / Illinois",
+ "class": "region",
+ "query": "Data center developments in Illinois (Chicago, Elk Grove Village, Aurora, Joliet): ComEd power capacity and interconnection, Cook and DuPage county approvals, Illinois tax incentive programs. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Chicago, IL",
+ "states": [
+ "IL"
+ ]
+ },
+ "region-nymetro": {
+ "monitorId": "",
+ "name": "New York Metro",
+ "class": "region",
+ "query": "Data center developments in New Jersey and New York: PSE&G and ConEd interconnection capacity, NJ Board of Public Utilities filings, NYC and Newark zoning, New York State moratorium proposals. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "New York Metro",
+ "states": [
+ "NJ",
+ "NY"
+ ]
+ },
+ "region-newengland": {
+ "monitorId": "",
+ "name": "New England",
+ "class": "region",
+ "query": "Data center developments in New England (Massachusetts, Connecticut, New Hampshire): Eversource and National Grid power capacity, Boston-area and Hartford zoning, Maine renewable-powered DC proposals. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "New England",
+ "states": [
+ "MA",
+ "CT",
+ "NH",
+ "ME",
+ "RI",
+ "VT"
+ ]
+ },
+ "region-minnesota": {
+ "monitorId": "",
+ "name": "Minnesota",
+ "class": "region",
+ "query": "Data center developments in Minnesota (Minneapolis, Shakopee, Chaska): Xcel Energy power capacity, Shakopee and Scott County approvals, Minnesota legislative actions on data centers. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Minnesota",
+ "states": [
+ "MN"
+ ]
+ },
+ "region-michigan": {
+ "monitorId": "",
+ "name": "Michigan",
+ "class": "region",
+ "query": "Data center developments in Michigan (Detroit, Grand Rapids, West Michigan): DTE Energy and Consumers Energy power capacity, county economic incentives, Michigan Strategic Fund approvals. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Michigan",
+ "states": [
+ "MI"
+ ]
+ },
+ "region-kentucky": {
+ "monitorId": "",
+ "name": "Kentucky",
+ "class": "region",
+ "query": "Data center developments in Kentucky (Louisville, Lexington, Bowling Green): LG&E and KU power agreements, Kentucky economic development incentives, TVA-connected sites. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Kentucky",
+ "states": [
+ "KY"
+ ]
+ },
+ "region-nevada": {
+ "monitorId": "",
+ "name": "Las Vegas / Nevada",
+ "class": "region",
+ "query": "Data center developments in Nevada (Las Vegas, Reno, Henderson): NV Energy power capacity, Southern Nevada Water Authority restrictions, Clark County approvals, Switch and other operator expansions. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Nevada",
+ "states": [
+ "NV"
+ ]
+ },
+ "region-dcmetro": {
+ "monitorId": "",
+ "name": "DC Metro / Maryland",
+ "class": "region",
+ "query": "Data center developments in Maryland and Washington DC metro (Prince George's County, Frederick, Loudoun adjacent): BGE and Pepco power capacity, PJM interconnection for MD sites, county zoning, Frederick County data center ordinances. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "DC Metro",
+ "states": [
+ "MD",
+ "DC",
+ "DE"
+ ]
+ },
+ "region-tennessee": {
+ "monitorId": "",
+ "name": "Tennessee",
+ "class": "region",
+ "query": "Data center developments in Tennessee (Nashville, Clarksville, Chattanooga): TVA power agreements and industrial rates, Clarksville campus developments, state economic incentives. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Tennessee",
+ "states": [
+ "TN"
+ ]
+ },
+ "region-midwest": {
+ "monitorId": "",
+ "name": "Kansas City / Midwest",
+ "class": "region",
+ "query": "Data center developments in the central Midwest (Missouri, Kansas, Nebraska, Iowa): Evergy and MidAmerican Energy power capacity, Kansas City metro approvals, Iowa and Nebraska economic incentives. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Central Midwest",
+ "states": [
+ "MO",
+ "KS",
+ "NE",
+ "IA"
+ ]
+ },
+ "region-carolinas": {
+ "monitorId": "",
+ "name": "Carolinas",
+ "class": "region",
+ "query": "Data center developments in North and South Carolina: Duke Energy power capacity and interconnection, Charlotte and Raleigh-Durham area approvals, Stokes County and rural county rezoning controversies. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Carolinas",
+ "states": [
+ "NC",
+ "SC"
+ ]
+ },
+ "region-colorado": {
+ "monitorId": "",
+ "name": "Colorado",
+ "class": "region",
+ "query": "Data center developments in Colorado (Denver, Aurora, Colorado Springs): Xcel Energy power capacity, Aurora and Douglas County approvals, Colorado water court filings. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Colorado",
+ "states": [
+ "CO"
+ ]
+ },
+ "region-pennsylvania": {
+ "monitorId": "",
+ "name": "Pennsylvania",
+ "class": "region",
+ "query": "Data center developments in Pennsylvania (Philadelphia, Lehigh Valley, Pittsburgh): PECO and PPL Electric power capacity, Lehigh Valley zoning, Pennsylvania economic incentive programs. Track: utility interconnection and power capacity filings, rezoning and special-use permit applications and decisions, moratoria or development freezes, community opposition and litigation, water-use restrictions or drought actions, large land assemblies and site transactions, hyperscaler expansion announcements, operator M&A or ownership changes, and construction milestones.",
+ "frequency": "1h",
+ "region": "Pennsylvania",
+ "states": [
+ "PA"
+ ]
+ },
+ "facility-qts-cedar-rapids": {
+ "monitorId": "",
+ "name": "QTS Cedar Rapids",
+ "class": "facility",
+ "query": "QTS Data Centers Cedar Rapids, Iowa campus: Alliant Energy and ITC Midwest power agreements, Linn County approvals, construction progress. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Iowa",
+ "facilityCode": "QTS-CR",
+ "states": [
+ "IA"
+ ]
+ },
+ "facility-qts-new-albany": {
+ "monitorId": "",
+ "name": "QTS New Albany",
+ "class": "facility",
+ "query": "QTS Data Centers New Albany, Ohio campus expansion: AEP Ohio power agreements, New Albany and Licking County zoning, construction milestones. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Central Ohio",
+ "facilityCode": "QTS-NA",
+ "states": [
+ "OH"
+ ]
+ },
+ "facility-qts-eagle-mountain": {
+ "monitorId": "",
+ "name": "QTS Eagle Mountain",
+ "class": "facility",
+ "query": "QTS Data Centers Eagle Mountain, Utah campus: Rocky Mountain Power commitments, Eagle Mountain City water rights and municipal actions. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Utah",
+ "facilityCode": "QTS-EM",
+ "states": [
+ "UT"
+ ]
+ },
+ "facility-qts-manassas": {
+ "monitorId": "",
+ "name": "QTS Manassas",
+ "class": "facility",
+ "query": "QTS Data Centers Manassas, Virginia expansion: Prince William County zoning votes, Dominion Energy interconnection filings, community opposition. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Northern Virginia",
+ "facilityCode": "QTS-MAN",
+ "states": [
+ "VA"
+ ]
+ },
+ "facility-qts-fayetteville": {
+ "monitorId": "",
+ "name": "QTS Fayetteville",
+ "class": "facility",
+ "query": "QTS Data Centers Fayetteville and Atlanta, Georgia campuses: Fayette County rezoning, Georgia Power interconnection, local government actions. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Atlanta, GA",
+ "facilityCode": "QTS-FAY",
+ "states": [
+ "GA"
+ ]
+ },
+ "facility-qts-aurora": {
+ "monitorId": "",
+ "name": "QTS Aurora-Denver",
+ "class": "facility",
+ "query": "QTS Data Centers Aurora, Colorado campus expansion: Xcel Energy power agreements, Aurora city approvals, construction milestones. Track any material development including power interconnection filings, zoning or permit actions, construction milestones, ownership or tenant changes, community opposition, and water or environmental restrictions.",
+ "frequency": "1h",
+ "region": "Colorado",
+ "facilityCode": "QTS-AUR",
+ "states": [
+ "CO"
+ ]
+ },
+ "discovery-hyperscale": {
+ "monitorId": "",
+ "name": "New Hyperscale Sites",
+ "class": "discovery",
+ "query": "Newly disclosed or rumored hyperscale data center projects in the U.S.: large land assemblies (500+ acres), new substation load studies, county rezoning filings consistent with 200MW+ campuses, and reports of hyperscaler site selection activity where no operator has confirmed. Include brownfield or failed-industrial sites with grid access being repurposed for data centers.",
+ "frequency": "1h",
+ "region": "National"
+ },
+ "discovery-power-markets": {
+ "monitorId": "",
+ "name": "Power-First Emerging Markets",
+ "class": "discovery",
+ "query": "U.S. regions newly attracting data center investment due to available power: utility announcements of large-load data center customers, new generation (gas, nuclear, SMR) tied to data center campuses, and secondary markets (Louisiana, Mississippi, Indiana, Wyoming, Alabama, Idaho, Wisconsin) seeing their first large-scale data center proposals or groundbreakings.",
+ "frequency": "1h",
+ "region": "National"
+ }
+}
\ No newline at end of file
diff --git a/typescript-recipes/parallel-datacenter-map/src/data/snapshot-monitors.json b/typescript-recipes/parallel-datacenter-map/src/data/snapshot-monitors.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/data/snapshot-monitors.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/typescript-recipes/parallel-datacenter-map/src/hooks/useDatacenters.ts b/typescript-recipes/parallel-datacenter-map/src/hooks/useDatacenters.ts
new file mode 100644
index 0000000..7af7557
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/hooks/useDatacenters.ts
@@ -0,0 +1,63 @@
+import { useMemo } from "react";
+import { datacenters } from "@/data/datacenters";
+import type { Datacenter, DisplayStatus } from "@/lib/types";
+import { toDisplayStatus } from "@/lib/utils";
+
+interface UseDatacentersResult {
+ filtered: Datacenter[];
+ /** Lifecycle counts within the current scope (all vs AI-only). */
+ counts: Record;
+ /** Total AI datacenters across the whole dataset (scope-independent). */
+ aiCount: number;
+ /** Total facilities across the whole dataset (scope-independent). */
+ totalCount: number;
+}
+
+export function isAiFacility(dc: Datacenter): boolean {
+ return !!dc.aiClassification && dc.aiClassification.ai_class !== "not-ai";
+}
+
+export function useDatacenters(
+ activeFilter: DisplayStatus | "all",
+ searchQuery: string,
+ aiOnly = false
+): UseDatacentersResult {
+ return useMemo(() => {
+ const counts: Record = {
+ all: 0, operational: 0, construction: 0, planned: 0, unknown: 0, decommissioned: 0,
+ };
+ let aiCount = 0;
+ let totalCount = 0;
+
+ const query = searchQuery.toLowerCase().trim();
+
+ const filtered = datacenters.filter((dc) => {
+ const display = toDisplayStatus(dc.status);
+ const isAi = isAiFacility(dc);
+
+ // Scope-independent totals (drive the scope toggle)
+ totalCount++;
+ if (isAi) aiCount++;
+
+ // Scope filter: AI-only excludes non-AI entirely (from counts + results)
+ if (aiOnly && !isAi) return false;
+
+ // Lifecycle counts, computed WITHIN the current scope
+ counts[display]++;
+ counts.all++;
+
+ // Apply lifecycle status filter
+ if (activeFilter !== "all" && display !== activeFilter) return false;
+
+ // Apply search
+ if (query) {
+ const searchable = `${dc.name} ${dc.operator} ${dc.owner} ${dc.city} ${dc.state} ${dc.region}`.toLowerCase();
+ if (!searchable.includes(query)) return false;
+ }
+
+ return true;
+ });
+
+ return { filtered, counts, aiCount, totalCount };
+ }, [activeFilter, searchQuery, aiOnly]);
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/hooks/useLiveTimer.ts b/typescript-recipes/parallel-datacenter-map/src/hooks/useLiveTimer.ts
new file mode 100644
index 0000000..771cdbf
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/hooks/useLiveTimer.ts
@@ -0,0 +1,38 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+
+const SYNC_INTERVAL = 30;
+
+export function useLiveTimer(onTick?: () => void) {
+ const [syncTime, setSyncTime] = useState(new Date());
+ const [countdown, setCountdown] = useState(SYNC_INTERVAL);
+
+ const handleTick = useCallback(() => {
+ setSyncTime(new Date());
+ onTick?.();
+ }, [onTick]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCountdown((prev) => {
+ if (prev <= 1) {
+ handleTick();
+ return SYNC_INTERVAL;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, [handleTick]);
+
+ const timeStr = syncTime.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: true,
+ });
+
+ return { timeStr, countdown };
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/hooks/useMonitors.ts b/typescript-recipes/parallel-datacenter-map/src/hooks/useMonitors.ts
new file mode 100644
index 0000000..66c3825
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/hooks/useMonitors.ts
@@ -0,0 +1,119 @@
+"use client";
+
+import { useState, useEffect, useCallback, useRef } from "react";
+import type { Monitor } from "@/lib/types";
+
+export interface SnapshotUpdate {
+ facilityIndex: string;
+ facilityName: string;
+ timestamp: string;
+ changedFields: string[];
+ changes?: Record;
+ /** Per changed field: reasoning + sources from the re-verification run. */
+ basis?: Record;
+}
+
+export function useMonitors() {
+ const [monitors, setMonitors] = useState([]);
+ const [lastChecked, setLastChecked] = useState(new Date());
+ const [loading, setLoading] = useState(true);
+ const [snapshotUpdates, setSnapshotUpdates] = useState>({});
+ const eventSourceRef = useRef(null);
+
+ const fetchMonitors = useCallback(async () => {
+ try {
+ const res = await fetch("/api/monitors", { cache: "no-store" });
+ if (!res.ok) return;
+ const data: Monitor[] = await res.json();
+ setMonitors(data);
+ setLastChecked(new Date());
+ setLoading(false);
+ } catch {
+ setLoading(false);
+ }
+ }, []);
+
+ // Fetch existing snapshot updates on mount
+ const fetchSnapshots = useCallback(async () => {
+ try {
+ const res = await fetch("/api/snapshots", { cache: "no-store" });
+ if (!res.ok) return;
+ const data = await res.json();
+ if (data.updates?.length) {
+ setSnapshotUpdates((prev) => {
+ const next = { ...prev };
+ for (const u of data.updates) {
+ next[u.facilityIndex] = {
+ facilityIndex: u.facilityIndex,
+ facilityName: u.facilityName,
+ timestamp: u.timestamp,
+ changedFields: u.changedFields,
+ changes: u.changes,
+ basis: u.basis,
+ };
+ }
+ return next;
+ });
+ }
+ } catch {
+ // ignore
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchMonitors();
+ fetchSnapshots();
+
+ // SSE for real-time webhook events
+ const es = new EventSource("/api/webhook");
+ eventSourceRef.current = es;
+
+ es.onmessage = (msg) => {
+ try {
+ const event = JSON.parse(msg.data);
+
+ // Track snapshot updates
+ if (event.facilityIndex && event.changedFields?.length > 0) {
+ setSnapshotUpdates((prev) => ({
+ ...prev,
+ [event.facilityIndex]: {
+ facilityIndex: event.facilityIndex,
+ facilityName: event.facilityName || "",
+ timestamp: event.receivedAt || new Date().toISOString(),
+ changedFields: event.changedFields,
+ changes: event.changes,
+ },
+ }));
+ }
+
+ // Refetch monitors for event_stream events
+ if (event.type === "monitor.event.detected" && !event.facilityIndex) {
+ fetchMonitors();
+ }
+ } catch {
+ // ignore parse errors
+ }
+ };
+
+ // Fallback poll
+ const fallback = setInterval(fetchMonitors, 60_000);
+
+ return () => {
+ es.close();
+ clearInterval(fallback);
+ };
+ }, [fetchMonitors]);
+
+ const totalEvents = monitors.reduce((s, m) => s + m.events.length, 0);
+ const totalFacilities = monitors.reduce((s, m) => s + m.facilityCount, 0);
+
+ return {
+ monitors,
+ lastChecked,
+ totalEvents,
+ totalFacilities,
+ loading,
+ snapshotUpdates,
+ refetch: fetchMonitors,
+ };
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/lib/constants.ts b/typescript-recipes/parallel-datacenter-map/src/lib/constants.ts
new file mode 100644
index 0000000..36d8a43
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/lib/constants.ts
@@ -0,0 +1,178 @@
+import type {
+ AiClass,
+ CommunityPushback,
+ DisplayStatus,
+ ImpactLevel,
+ MonitorCategory,
+} from "./types";
+
+export const STATUS_MAP: Record = {
+ operational: "operational",
+ "under-construction": "construction",
+ planned: "planned",
+ unknown: "unknown",
+ decommissioned: "decommissioned",
+};
+
+export const STATUS_COLORS: Record = {
+ operational: "#FB631B",
+ construction: "#F79A6F",
+ planned: "#5C5B59",
+ unknown: "#D6D6D6",
+ decommissioned: "#434343",
+};
+
+export const STATUS_LABELS: Record = {
+ operational: "Operational",
+ construction: "Under Construction",
+ planned: "Planned",
+ unknown: "Unknown",
+ decommissioned: "Decommissioned",
+};
+
+export const MONITOR_CATEGORY_LABELS: Record = {
+ POWER_GRID: "Power & Grid",
+ ZONING_POLICY: "Zoning & Policy",
+ COMMUNITY: "Community",
+ WATER: "Water & Cooling",
+ LAND_SUPPLY: "Land & Supply",
+ TENANT_DEMAND: "Tenant & Demand",
+ CAPITAL_OWNERSHIP: "Capital & Ownership",
+ CONSTRUCTION: "Construction",
+};
+
+export const MONITOR_CATEGORY_COLORS: Record = {
+ POWER_GRID: "#FB631B",
+ ZONING_POLICY: "#F79A6F",
+ COMMUNITY: "#5C5B59",
+ WATER: "#8FB6CC",
+ LAND_SUPPLY: "#D8D0BF",
+ TENANT_DEMAND: "#FB631B",
+ CAPITAL_OWNERSHIP: "#E14942",
+ CONSTRUCTION: "#858483",
+};
+
+export const SEVERITY_COLORS: Record = {
+ critical: "#E14942",
+ notable: "#FB631B",
+ informational: "#858483",
+};
+
+export const AI_CLASS_LABELS: Record = {
+ "ai-training": "AI Training",
+ "ai-inference": "AI Inference",
+ "ai-mixed": "AI Mixed Use",
+ "cloud-hyperscale": "Cloud Hyperscale",
+ "not-ai": "Colo / Enterprise",
+};
+
+export const AI_CLASS_COLORS: Record = {
+ "ai-training": "#1D1B16",
+ "ai-inference": "#434343",
+ "ai-mixed": "#5C5B59",
+ "cloud-hyperscale": "#858483",
+ "not-ai": "#ADADAC",
+};
+
+export const IMPACT_LABELS: Record = {
+ high: "High",
+ moderate: "Moderate",
+ low: "Low",
+ unknown: "Unknown",
+};
+
+export const IMPACT_COLORS: Record = {
+ high: "#E14942",
+ moderate: "#FB631B",
+ low: "#69BE78",
+ unknown: "#D6D6D6",
+};
+
+export const PUSHBACK_LABELS: Record = {
+ "active-opposition": "Active opposition",
+ "some-concern": "Some concern",
+ "none-found": "None found",
+};
+
+export const PUSHBACK_COLORS: Record = {
+ "active-opposition": "#E14942",
+ "some-concern": "#FB631B",
+ "none-found": "#69BE78",
+};
+
+/** Maps US states to the monitor that covers them */
+export const STATE_TO_MONITOR: Record = {
+ VA: "region-nova",
+ GA: "region-atlanta",
+ OH: "region-ohio",
+ AZ: "region-phoenix",
+ UT: "region-utah",
+ TX: "region-texas",
+ WA: "region-pnw",
+ OR: "region-pnw",
+ FL: "region-florida",
+ IL: "region-chicago",
+ NJ: "region-nymetro",
+ NY: "region-nymetro",
+ MA: "region-newengland",
+ CT: "region-newengland",
+ NH: "region-newengland",
+ ME: "region-newengland",
+ RI: "region-newengland",
+ VT: "region-newengland",
+ MN: "region-minnesota",
+ MI: "region-michigan",
+ KY: "region-kentucky",
+ NV: "region-nevada",
+ MD: "region-dcmetro",
+ DC: "region-dcmetro",
+ DE: "region-dcmetro",
+ TN: "region-tennessee",
+ MO: "region-midwest",
+ KS: "region-midwest",
+ NE: "region-midwest",
+ IA: "region-midwest",
+ NC: "region-carolinas",
+ SC: "region-carolinas",
+ CO: "region-colorado",
+ PA: "region-pennsylvania",
+ // CA needs special handling (NorCal vs SoCal by latitude)
+ CA: "region-norcal", // default; SoCal for lat < 35.5
+};
+
+/** Latitude threshold for NorCal vs SoCal */
+export const CA_SPLIT_LAT = 35.5;
+
+/** Region centroids for map flyTo */
+export const REGION_CENTROIDS: Record = {
+ "region-nova": [38.95, -77.45],
+ "region-atlanta": [33.75, -84.39],
+ "region-ohio": [40.05, -82.75],
+ "region-phoenix": [33.45, -112.07],
+ "region-utah": [40.55, -111.90],
+ "region-texas": [31.0, -97.5],
+ "region-pnw": [47.6, -122.3],
+ "region-florida": [28.5, -81.5],
+ "region-norcal": [37.4, -122.0],
+ "region-socal": [34.0, -118.2],
+ "region-chicago": [41.88, -87.63],
+ "region-nymetro": [40.7, -74.2],
+ "region-newengland": [42.36, -71.06],
+ "region-minnesota": [44.97, -93.27],
+ "region-michigan": [42.33, -83.05],
+ "region-kentucky": [38.25, -85.76],
+ "region-nevada": [36.17, -115.14],
+ "region-dcmetro": [39.0, -76.7],
+ "region-tennessee": [36.16, -86.78],
+ "region-midwest": [39.1, -94.58],
+ "region-carolinas": [35.78, -78.64],
+ "region-colorado": [39.74, -104.99],
+ "region-pennsylvania": [40.0, -75.5],
+};
+
+export const MAP_CENTER: [number, number] = [39.8283, -98.5795];
+export const MAP_ZOOM = 5;
+export const TILE_URL =
+ "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
+export const TILE_ATTRIBUTION =
+ '© OpenStreetMap © CARTO';
diff --git a/typescript-recipes/parallel-datacenter-map/src/lib/newsletter-writer.ts b/typescript-recipes/parallel-datacenter-map/src/lib/newsletter-writer.ts
new file mode 100644
index 0000000..d82960a
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/lib/newsletter-writer.ts
@@ -0,0 +1,259 @@
+/**
+ * Newsletter writer agent: Claude writes polished HTML email newsletters
+ * using deep research from the Parallel Task API, with a parallel_lookup
+ * tool for fact-checking via Task API base with interaction chaining.
+ */
+
+import Anthropic from "@anthropic-ai/sdk";
+
+const PARALLEL_BASE = "https://api.parallel.ai";
+
+const NEWSLETTER_SYSTEM = `You are the editor of "Datacenter Signal," a weekly intelligence brief for datacenter infrastructure investors. Your job is to transform raw research into a polished, professional HTML email newsletter.
+
+VOICE: Analytical, concise, data-anchored. Write like a Financial Times or Stratechery briefing. No hype, no speculation. Evidence and clarity. No emoji.
+
+STRUCTURE (follow exactly):
+1. MASTHEAD — already provided in the template, don't generate
+2. THE WEEK IN ONE READ — 2-3 sentence executive summary of the most important theme
+3. CRITICAL DEVELOPMENTS — for each critical event:
+ - Category tag + region
+ - Headline (bold, 18px)
+ - 2-3 paragraphs of analysis: what happened, background context, implications for investors, what to watch
+ - Inline source links woven into prose (e.g., "according to Virginia Mercury")
+ - NEVER use numbered references like [1] or [27]
+4. REGIONAL ROUNDUP — one line per active region, the most important headline
+5. BY THE NUMBERS — 8-12 key data points as a clean list
+
+CITATION RULES — THIS IS THE MOST IMPORTANT PART:
+- Cite AGGRESSIVELY. The research provides a large SOURCE POOL of real URLs — use as many DISTINCT sources as you can. A strong issue links 25-40+ distinct sources. Sparse linking is a failure.
+- EVERY factual sentence — every number, date, dollar figure, vote count, company name, quote, or claim — MUST carry an inline link to a source from the pool.
+- Prefer 2-3 links per paragraph over one. When multiple sources support a point, link several ("Reuters and the Virginia Mercury both report…").
+- Use the publication/domain name as the link text, not the article title.
+- Only use URLs that appear in the SOURCE POOL or research below — never invent a URL. Match each link to the most relevant source.
+- Do NOT use numbered references like [1] or [27] — always inline hyperlinks.
+
+HTML FORMAT:
+- Use inline styles only (email-safe)
+- Headings:
+- Body:
+- Links:
+- Bold:
+- Lists: -
+- Category tags: CATEGORY
+ Colors: Power & Grid=#FB631B, Zoning & Policy=#F79A6F, Capital & Ownership=#E14942, Community=#5C5B59, Construction=#858483
+
+OUTPUT: Return ONLY the HTML content for the body section (between masthead and footer — those are added separately). No markdown. Pure HTML with inline styles.`;
+
+const TOOLS: Anthropic.Tool[] = [
+ {
+ name: "parallel_lookup",
+ description:
+ "Look up a specific fact, verify a claim, or find missing information using Parallel's research API. This tool has full context from the deep research already performed. Use it for: verifying exact numbers, finding primary source URLs, checking dates, getting vote counts, confirming deal values, etc. Ask a clear, specific question.",
+ input_schema: {
+ type: "object" as const,
+ properties: {
+ query: {
+ type: "string",
+ description:
+ "A specific factual question to look up, e.g., 'What was the exact vote count for the Loudoun County data center moratorium?' or 'What is the dollar amount of the NextEra-Dominion merger?'",
+ },
+ },
+ required: ["query"],
+ },
+ },
+];
+
+async function callParallelLookup(
+ query: string,
+ interactionId: string,
+ apiKey: string,
+): Promise {
+ console.log(` [lookup] ${query.slice(0, 80)}...`);
+ const res = await fetch(`${PARALLEL_BASE}/v1/tasks/runs`, {
+ method: "POST",
+ headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ input: query,
+ processor: "base",
+ previous_interaction_id: interactionId,
+ }),
+ });
+ if (!res.ok) return `Error: ${res.status}`;
+ const task = await res.json();
+
+ const start = Date.now();
+ while (Date.now() - start < 120000) {
+ await new Promise((r) => setTimeout(r, 3000));
+ const statusRes = await fetch(
+ `${PARALLEL_BASE}/v1/tasks/runs/${task.run_id}`,
+ { headers: { "x-api-key": apiKey } },
+ );
+ if (!statusRes.ok) continue;
+ const data = await statusRes.json();
+ if (data.status === "completed") {
+ const resultRes = await fetch(
+ `${PARALLEL_BASE}/v1/tasks/runs/${task.run_id}/result`,
+ { headers: { "x-api-key": apiKey } },
+ );
+ const result = await resultRes.json();
+ const raw = result.output?.content;
+ return typeof raw === "string"
+ ? raw
+ : JSON.stringify(raw) || "No result found.";
+ }
+ if (data.status === "failed") return "Lookup failed.";
+ }
+ return "Lookup timed out.";
+}
+
+export async function writeNewsletter(opts: {
+ research: string;
+ interactionId: string;
+ issueNumber: number;
+ eventsTotal: number;
+ criticalCount: number;
+ marketsActive: number;
+ regionSummaries: string;
+ parallelApiKey: string;
+ anthropicApiKey: string;
+ citationPool?: { title: string; url: string }[];
+}): Promise {
+ const {
+ research,
+ interactionId,
+ issueNumber,
+ eventsTotal,
+ criticalCount,
+ marketsActive,
+ regionSummaries,
+ parallelApiKey,
+ anthropicApiKey,
+ citationPool = [],
+ } = opts;
+
+ // Dedupe the pool by URL and cap it so the prompt stays manageable
+ const pool = Array.from(
+ new Map(citationPool.filter((c) => c.url).map((c) => [c.url, c])).values()
+ ).slice(0, 120);
+ const poolBlock = pool.length
+ ? pool.map((c, i) => `${i + 1}. ${c.title || "Source"} — ${c.url}`).join("\n")
+ : "(none supplied — pull URLs from the research text below)";
+
+ const userMessage = `Write Datacenter Signal Issue ${issueNumber}.
+
+STATS: ${eventsTotal} total events, ${criticalCount} critical, ${marketsActive} markets active.
+
+SOURCE POOL — ${pool.length} real citations gathered by the Task API. Weave as many of these as possible into the prose as inline hyperlinks. Reuse the exact URLs:
+${poolBlock}
+
+DEEP RESEARCH OUTPUT (your primary narrative source — already fact-checked, contains additional inline URLs):
+${research}
+
+ALL MONITOR EVENTS (for the regional roundup — one line per region):
+${regionSummaries}
+
+Write the complete HTML email body now, linking densely from the SOURCE POOL. Use the parallel_lookup tool only if you must verify a specific fact.`;
+
+ const anthropic = new Anthropic({ apiKey: anthropicApiKey });
+ let messages: Anthropic.MessageParam[] = [
+ { role: "user", content: userMessage },
+ ];
+ let finalHtml = "";
+
+ for (let turn = 0; turn < 12; turn++) {
+ const response = await anthropic.messages.create({
+ model: "claude-sonnet-5",
+ max_tokens: 20000,
+ system: NEWSLETTER_SYSTEM,
+ tools: TOOLS,
+ messages,
+ });
+
+ console.log(
+ ` [writer] Turn ${turn + 1}: stop=${response.stop_reason}, blocks=${response.content.length}`,
+ );
+
+ const toolUseBlocks = response.content.filter(
+ (b) => b.type === "tool_use",
+ ) as Anthropic.ToolUseBlock[];
+
+ // Keep the LONGEST text block seen — the finished HTML is far longer than
+ // any planning/preamble text, so this survives multi-turn tool use without
+ // being clobbered by a short "I'll verify a few facts…" preamble.
+ for (const block of response.content) {
+ if (block.type === "text" && block.text.length > finalHtml.length) {
+ finalHtml = block.text;
+ }
+ }
+
+ if (toolUseBlocks.length === 0 || response.stop_reason === "end_turn") {
+ break;
+ }
+
+ // Execute all tool calls in parallel for speed
+ const lookupResults = await Promise.all(
+ toolUseBlocks.map(async (block) => {
+ const query = (block.input as { query: string }).query;
+ const result = await callParallelLookup(
+ query,
+ interactionId,
+ parallelApiKey,
+ );
+ return { tool_use_id: block.id, result };
+ }),
+ );
+
+ const toolResultContent = lookupResults.map((r) => ({
+ type: "tool_result" as const,
+ tool_use_id: r.tool_use_id,
+ content: [{ type: "text" as const, text: r.result }],
+ }));
+
+ messages = [
+ ...messages,
+ { role: "assistant", content: response.content },
+ { role: "user", content: toolResultContent },
+ ];
+ }
+
+ // Safety net: if the loop exhausted its turns on tool use before producing a
+ // full document, force one final write with no tools available.
+ if (finalHtml.replace(/```html|```/g, "").trim().length < 1500) {
+ console.log(" [writer] Output too short — forcing a final no-tools write.");
+ const forced = await anthropic.messages.create({
+ model: "claude-sonnet-5",
+ max_tokens: 20000,
+ system: NEWSLETTER_SYSTEM,
+ messages: [
+ ...messages,
+ { role: "user", content: "Write the complete HTML email body now using everything gathered above. Link densely from the SOURCE POOL. Output only the HTML." },
+ ],
+ });
+ const text = forced.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("\n");
+ if (text.length > finalHtml.length) finalHtml = text;
+ }
+
+ // Extract HTML from code fences if Claude wrapped it
+ const fenceMatch = finalHtml.match(/```html\s*([\s\S]*?)```/);
+ if (fenceMatch) finalHtml = fenceMatch[1].trim();
+
+ return finalHtml;
+}
+
+export function wrapEmailTemplate(
+ bodyHtml: string,
+ issueNumber: number,
+): string {
+ return `
+
+
parallel
+
+Datacenter Signal
+Issue ${issueNumber}
+
+
${bodyHtml}
+
+
parallel
+
hello@parallel.ai · Palo Alto, CA
+
`;
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/lib/types.ts b/typescript-recipes/parallel-datacenter-map/src/lib/types.ts
new file mode 100644
index 0000000..fee86cb
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/lib/types.ts
@@ -0,0 +1,169 @@
+export interface Datacenter {
+ name: string;
+ operator: string;
+ owner: string;
+ address: string;
+ city: string;
+ state: string;
+ zip: string;
+ lat: number;
+ lng: number;
+ yearOnline: string;
+ powerMw: number;
+ sqft: number;
+ type: string;
+ status: DatacenterStatus;
+ region: string;
+ enrichment?: DatacenterEnrichment;
+ aiClassification?: AiClassification;
+ /** Original row index in the source dataset — the key for enrichment,
+ * AI-classification, snapshot, and per-facility blob lookups. Survives
+ * dedup so those references stay valid after duplicate rows are collapsed. */
+ sourceIndex?: number;
+}
+
+export type AiClass =
+ | "ai-training"
+ | "ai-inference"
+ | "ai-mixed"
+ | "cloud-hyperscale"
+ | "not-ai";
+
+export type ImpactLevel = "high" | "moderate" | "low" | "unknown";
+
+export type CommunityPushback =
+ | "active-opposition"
+ | "some-concern"
+ | "none-found";
+
+export interface AiClassification {
+ ai_class: AiClass;
+ ai_evidence: string;
+ water_impact: ImpactLevel;
+ water_note: string;
+ grid_impact: ImpactLevel;
+ grid_note: string;
+ community_pushback: CommunityPushback;
+ community_note: string;
+ citations?: { title: string; url: string }[];
+ classifiedAt?: string;
+ runId?: string;
+}
+
+export interface DatacenterEnrichment {
+ // v1
+ description: string;
+ verified_status: string;
+ power_capacity_mw: number;
+ total_sqft: number;
+ year_online: string;
+ construction_update: string;
+ recent_news: string;
+ notable_tenants: string;
+ // v2
+ verified_name: string;
+ verified_operator: string;
+ verified_owner: string;
+ cooling_type: string;
+ tier_level: string;
+ fiber_providers: string;
+ num_buildings: number;
+ campus_acres: number;
+ utility_provider: string;
+ tax_incentives: string;
+ natural_hazard_zone: string;
+ // metadata
+ citations?: { field: string; url: string; title: string }[];
+ reasoning?: Record;
+ enrichedAt?: string;
+ runId?: string;
+}
+
+export type DatacenterStatus =
+ | "operational"
+ | "under-construction"
+ | "planned"
+ | "unknown"
+ | "decommissioned";
+
+export type DisplayStatus =
+ | "operational"
+ | "construction"
+ | "planned"
+ | "unknown"
+ | "decommissioned";
+
+export type EventCategory =
+ | "POWER & GRID"
+ | "OWNERSHIP"
+ | "NEW SITE"
+ | "PERMITS"
+ | "EXPANSION"
+ | "COMMUNITY"
+ | "WATER"
+ | "POLICY";
+
+export interface MonitorEvent {
+ id: string;
+ monitorId: string;
+ monitorName: string;
+ category: EventCategory;
+ facilityCode: string;
+ timestamp: string;
+ headline: string;
+ description: string;
+ sources: { label: string; url: string }[];
+ confidence?: number;
+ hasTaskReport?: boolean;
+ taskReportSummary?: string;
+ region?: string;
+ rawPayload?: unknown;
+}
+
+export type MonitorCategory =
+ | "POWER_GRID"
+ | "ZONING_POLICY"
+ | "COMMUNITY"
+ | "WATER"
+ | "LAND_SUPPLY"
+ | "TENANT_DEMAND"
+ | "CAPITAL_OWNERSHIP"
+ | "CONSTRUCTION";
+
+export interface Monitor {
+ id: string;
+ monitorId: string;
+ name: string;
+ class: "region" | "facility" | "discovery";
+ query: string;
+ frequency: string;
+ region?: string;
+ facilityCode?: string;
+ states?: string[];
+ facilityCount: number;
+ events: MonitorDetection[];
+}
+
+export interface MonitorDetection {
+ eventId: string;
+ eventDate: string;
+ category: MonitorCategory;
+ headline: string;
+ summary: string;
+ severity: "critical" | "notable" | "informational";
+ affectedEntities: string;
+ citations: { title: string; url: string; excerpts?: string[] }[];
+ rawPayload?: unknown;
+}
+
+export interface MonitorConfig {
+ id: string;
+ monitorId?: string;
+ name: string;
+ class: "region" | "facility" | "discovery";
+ query: string;
+ frequency: string;
+ processor: string;
+ region?: string;
+ facilityCode?: string;
+}
diff --git a/typescript-recipes/parallel-datacenter-map/src/lib/utils.ts b/typescript-recipes/parallel-datacenter-map/src/lib/utils.ts
new file mode 100644
index 0000000..5533f64
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/src/lib/utils.ts
@@ -0,0 +1,79 @@
+import type { DatacenterStatus, DisplayStatus } from "./types";
+import { STATUS_MAP } from "./constants";
+
+export function toDisplayStatus(status: DatacenterStatus): DisplayStatus {
+ return STATUS_MAP[status] || "unknown";
+}
+
+export function formatNumber(n: number): string {
+ if (n === 0) return "—";
+ return n.toLocaleString("en-US");
+}
+
+export function formatPower(mw: number): string {
+ if (mw === 0) return "—";
+ if (mw >= 1000) return `${(mw / 1000).toFixed(1)} GW`;
+ return `${mw.toFixed(1)} MW`;
+}
+
+export function formatSqft(sqft: number): string {
+ if (sqft === 0) return "—";
+ if (sqft >= 1000000) return `${(sqft / 1000000).toFixed(1)}M sq ft`;
+ return `${formatNumber(Math.round(sqft))} sq ft`;
+}
+
+/**
+ * Relative label for a DATE-ONLY value (e.g. a monitor event_date like
+ * "2026-07-05"). Monitor events carry no time component, so we only claim
+ * day-level precision — never fake "hours ago" from a bare date.
+ */
+export function relativeDate(dateStr: string): string {
+ const then = new Date(dateStr);
+ if (isNaN(then.getTime())) return dateStr;
+ const now = new Date();
+ const dayMs = 86_400_000;
+ const a = Date.UTC(then.getUTCFullYear(), then.getUTCMonth(), then.getUTCDate());
+ const b = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
+ const days = Math.round((b - a) / dayMs);
+ if (days <= 0) return "Today";
+ if (days === 1) return "Yesterday";
+ if (days < 7) return `${days}d ago`;
+ return `${Math.floor(days / 7)}w ago`;
+}
+
+export function timeAgo(dateStr: string): string {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+
+ if (diffMins < 1) return "just now";
+ if (diffMins < 60) return `${diffMins}m ago`;
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `${diffHours}h ago`;
+ const diffDays = Math.floor(diffHours / 24);
+ if (diffDays < 7) return `${diffDays}d ago`;
+ const diffWeeks = Math.floor(diffDays / 7);
+ return `${diffWeeks}w ago`;
+}
+
+export function classifyCategory(text: string): string {
+ const lower = text.toLowerCase();
+ if (lower.includes("power") || lower.includes("grid") || lower.includes("energy") || lower.includes("substation") || lower.includes("interconnection") || lower.includes("utility"))
+ return "POWER & GRID";
+ if (lower.includes("ownership") || lower.includes("acquisition") || lower.includes("sale") || lower.includes("acquire"))
+ return "OWNERSHIP";
+ if (lower.includes("new site") || lower.includes("rumor") || lower.includes("land assembl") || lower.includes("newly disclosed"))
+ return "NEW SITE";
+ if (lower.includes("permit") || lower.includes("zoning") || lower.includes("rezoning") || lower.includes("approval"))
+ return "PERMITS";
+ if (lower.includes("expansion") || lower.includes("construction") || lower.includes("build"))
+ return "EXPANSION";
+ if (lower.includes("community") || lower.includes("opposition") || lower.includes("moratori"))
+ return "COMMUNITY";
+ if (lower.includes("water"))
+ return "WATER";
+ if (lower.includes("policy") || lower.includes("legislation") || lower.includes("regulation"))
+ return "POLICY";
+ return "PERMITS";
+}
diff --git a/typescript-recipes/parallel-datacenter-map/tsconfig.json b/typescript-recipes/parallel-datacenter-map/tsconfig.json
new file mode 100644
index 0000000..7e0d0d8
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules", "scripts"]
+}
diff --git a/typescript-recipes/parallel-datacenter-map/vercel.json b/typescript-recipes/parallel-datacenter-map/vercel.json
new file mode 100644
index 0000000..0708500
--- /dev/null
+++ b/typescript-recipes/parallel-datacenter-map/vercel.json
@@ -0,0 +1,8 @@
+{
+ "crons": [
+ {
+ "path": "/api/cron/newsletter",
+ "schedule": "*/15 * * * 1"
+ }
+ ]
+}