← back to Govarbitrage
DigestSnapshot model: freeze each honesty-gated Top-10 at send time (public-safe fields only); capture wired into digest send + standalone; seed script with marked fabricated edition
343863bbc119b8fc706a0f10bff21d3a3be2f65b · 2026-07-13 01:38:44 -0700 · Steve Abrams
Files touched
M prisma/schema.prismaA scripts/seed-digest-snapshots.tsA src/lib/digest-snapshot.tsM src/lib/send-digest.ts
Diff
commit 343863bbc119b8fc706a0f10bff21d3a3be2f65b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 01:38:44 2026 -0700
DigestSnapshot model: freeze each honesty-gated Top-10 at send time (public-safe fields only); capture wired into digest send + standalone; seed script with marked fabricated edition
---
prisma/schema.prisma | 22 +++++++
scripts/seed-digest-snapshots.ts | 117 +++++++++++++++++++++++++++++++++++
src/lib/digest-snapshot.ts | 128 +++++++++++++++++++++++++++++++++++++++
src/lib/send-digest.ts | 6 ++
4 files changed, 273 insertions(+)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 870ab84..04de4aa 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -463,3 +463,25 @@ model AuditLog {
@@index([createdAt])
@@index([action])
}
+
+// ------------------------------------------------------------------ digest archive
+
+enum DigestSlot {
+ AM
+ PM
+}
+
+// Frozen record of each digest run. The public /deals archive pages render
+// these snapshots so archive URLs stay stable and indexable instead of
+// re-querying live listings that expire out from under a crawled page.
+model DigestSnapshot {
+ id String @id @default(cuid())
+ date String // YYYY-MM-DD in America/Los_Angeles (the digest's home timezone)
+ slot DigestSlot
+ dealsJson Json // public-safe serialized Top-10 (see src/lib/digest-snapshot.ts)
+ dealCount Int
+ createdAt DateTime @default(now())
+
+ @@unique([date, slot])
+ @@index([date])
+}
diff --git a/scripts/seed-digest-snapshots.ts b/scripts/seed-digest-snapshots.ts
new file mode 100644
index 0000000..bfae08d
--- /dev/null
+++ b/scripts/seed-digest-snapshots.ts
@@ -0,0 +1,117 @@
+// Seed the public /deals archive:
+// 1. A REAL capture for today's slot via captureDigestSnapshot() — freezes
+// whatever currently clears the hot-deal gate (may be 0 deals; the
+// archive renders that honestly).
+// 2. A clearly-marked FABRICATED edition for 2026-07-12 with realistic
+// deals so the teaser rendering (confidence labels, ROI bands, withheld
+// numbers) is actually exercised before launch. Every fabricated deal's
+// disclaimer says SEED DATA. Delete before go-live:
+// DELETE FROM "DigestSnapshot" WHERE date = '2026-07-12';
+//
+// Run: npx tsx scripts/seed-digest-snapshots.ts
+
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import type { Prisma } from "@prisma/client";
+
+function loadEnv() {
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+ try {
+ const raw = readFileSync(join(root, ".env"), "utf8");
+ for (const line of raw.split("\n")) {
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i);
+ if (!m) continue;
+ const key = m[1];
+ let val = m[2].trim();
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
+ val = val.slice(1, -1);
+ }
+ if (process.env[key] === undefined) process.env[key] = val;
+ }
+ } catch {
+ /* no .env — rely on ambient env */
+ }
+}
+loadEnv();
+
+const { prisma } = await import("../src/lib/db");
+const { captureDigestSnapshot, currentSlot } = await import("../src/lib/digest-snapshot");
+type SnapshotDeal = import("../src/lib/digest-snapshot").SnapshotDeal;
+
+const SEED_DATE = "2026-07-12";
+const SEED_DISCLAIMER = "SEED DATA — fabricated example for pre-launch rendering tests, not a real listing.";
+
+const seedDeals: SnapshotDeal[] = [
+ {
+ rank: 1,
+ title: "2018 John Deere 320G Skid Steer (1,240 hrs) — County Fleet Surplus",
+ source: "GOVDEALS",
+ locationCity: "Bakersfield",
+ locationState: "CA",
+ currentBid: 6200,
+ recMax: 14800,
+ expectedSaleLow: 21500,
+ confidence: "HIGH",
+ roiConservative: 0.58,
+ roiCapped: false,
+ disclaimer: SEED_DISCLAIMER,
+ closingAt: "2026-07-14T19:00:00.000Z",
+ sourceUrl: null,
+ },
+ {
+ rank: 2,
+ title: "Pallet of 24 Dell Latitude 5520 Laptops (i5/16GB, wiped) — School District IT Refresh",
+ source: "PUBLICSURPLUS",
+ locationCity: "Mesa",
+ locationState: "AZ",
+ currentBid: 1150,
+ recMax: 3400,
+ expectedSaleLow: 5300,
+ confidence: "MEDIUM",
+ roiConservative: 0.49,
+ roiCapped: false,
+ disclaimer: SEED_DISCLAIMER,
+ closingAt: "2026-07-13T22:30:00.000Z",
+ sourceUrl: null,
+ },
+ {
+ rank: 3,
+ title: "2015 Ford F-250 XL 4x4 Utility Truck w/ Liftgate — Municipal Water Dept.",
+ source: "MUNICIBID",
+ locationCity: "Spokane",
+ locationState: "WA",
+ currentBid: 3875,
+ recMax: 9200,
+ expectedSaleLow: 12800,
+ confidence: "LOW",
+ roiConservative: 0.41,
+ roiCapped: true,
+ disclaimer: SEED_DISCLAIMER,
+ closingAt: null, // make-offer, no deadline
+ sourceUrl: null,
+ },
+];
+
+async function main() {
+ const real = await captureDigestSnapshot(currentSlot());
+ console.log(`Real capture: ${real.date} ${real.slot} — ${real.dealCount} deal(s)`);
+
+ const dealsJson = seedDeals as unknown as Prisma.InputJsonValue;
+ const fabricated = await prisma.digestSnapshot.upsert({
+ where: { date_slot: { date: SEED_DATE, slot: "AM" } },
+ create: { date: SEED_DATE, slot: "AM", dealsJson, dealCount: seedDeals.length },
+ update: { dealsJson, dealCount: seedDeals.length },
+ });
+ console.log(`Fabricated seed edition: ${fabricated.date} ${fabricated.slot} — ${fabricated.dealCount} deal(s) (SEED DATA)`);
+}
+
+main()
+ .then(async () => {
+ await prisma.$disconnect();
+ })
+ .catch(async (e) => {
+ console.error(e);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/src/lib/digest-snapshot.ts b/src/lib/digest-snapshot.ts
new file mode 100644
index 0000000..b3dc35d
--- /dev/null
+++ b/src/lib/digest-snapshot.ts
@@ -0,0 +1,128 @@
+import { prisma } from "@/lib/db";
+import { findHotDeals, type HotDeal, type Confidence } from "@/lib/hot-deals";
+import type { DigestSlot, Prisma } from "@prisma/client";
+
+// Freezes each digest run into a DigestSnapshot so the public /deals archive
+// renders a stable, indexable record instead of re-querying live listings
+// that expire. Only PUBLIC-SAFE display fields are serialized — exactly what
+// the free-tier digest email already shows, never internal cost math.
+
+export interface SnapshotDeal {
+ rank: number;
+ title: string;
+ source: string;
+ locationCity: string | null;
+ locationState: string | null;
+ currentBid: number;
+ recMax: number;
+ expectedSaleLow: number;
+ confidence: Confidence;
+ roiConservative: number;
+ roiCapped: boolean;
+ disclaimer: string;
+ closingAt: string | null;
+ sourceUrl: string | null;
+}
+
+export function digestDateString(d = new Date()): string {
+ // en-CA renders YYYY-MM-DD; PT is the digest's home timezone (6am/5pm sends).
+ return d.toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" });
+}
+
+export function currentSlot(d = new Date()): DigestSlot {
+ const hour = Number(
+ d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", hour12: false }),
+ );
+ return hour < 12 ? "AM" : "PM";
+}
+
+export function toSnapshotDeals(deals: HotDeal[]): SnapshotDeal[] {
+ return deals.map((d, i) => ({
+ rank: i + 1,
+ title: d.listing.title,
+ source: d.listing.source,
+ locationCity: d.listing.locationCity,
+ locationState: d.listing.locationState,
+ currentBid: d.currentBid,
+ recMax: d.recMax,
+ expectedSaleLow: d.expectedSaleLow,
+ confidence: d.confidence,
+ roiConservative: d.roiConservative,
+ roiCapped: d.roiCapped,
+ disclaimer: d.disclaimer,
+ closingAt: d.closingAt ? new Date(d.closingAt).toISOString() : null,
+ sourceUrl: d.listing.sourceUrl,
+ }));
+}
+
+/** Persist a snapshot from already-fetched deals (used at digest send time). */
+export async function persistDigestSnapshot(slot: DigestSlot, deals: HotDeal[], date = digestDateString()) {
+ const snapshotDeals = toSnapshotDeals(deals);
+ const dealsJson = snapshotDeals as unknown as Prisma.InputJsonValue;
+ return prisma.digestSnapshot.upsert({
+ where: { date_slot: { date, slot } },
+ create: { date, slot, dealsJson, dealCount: snapshotDeals.length },
+ update: { dealsJson, dealCount: snapshotDeals.length },
+ });
+}
+
+/** Standalone capture: run the honesty-gated Top-10 and freeze it. */
+export async function captureDigestSnapshot(slot: DigestSlot = currentSlot()) {
+ const deals = await findHotDeals({ limit: 10 });
+ return persistDigestSnapshot(slot, deals);
+}
+
+// ── read side for the /deals archive pages ─────────────────────────────────
+
+export interface DigestEdition {
+ date: string;
+ slot: DigestSlot;
+ dealCount: number;
+ createdAt: Date;
+ deals: SnapshotDeal[];
+}
+
+function parseDeals(dealsJson: Prisma.JsonValue): SnapshotDeal[] {
+ return Array.isArray(dealsJson) ? (dealsJson as unknown as SnapshotDeal[]) : [];
+}
+
+export async function listDigestEditions(limit = 60): Promise<DigestEdition[]> {
+ const rows = await prisma.digestSnapshot.findMany({
+ orderBy: [{ date: "desc" }, { slot: "asc" }],
+ take: limit,
+ });
+ return rows.map((r) => ({
+ date: r.date,
+ slot: r.slot,
+ dealCount: r.dealCount,
+ createdAt: r.createdAt,
+ deals: parseDeals(r.dealsJson),
+ }));
+}
+
+export async function getDigestEditionsForDate(date: string): Promise<DigestEdition[]> {
+ const rows = await prisma.digestSnapshot.findMany({
+ where: { date },
+ orderBy: { slot: "asc" },
+ });
+ return rows.map((r) => ({
+ date: r.date,
+ slot: r.slot,
+ dealCount: r.dealCount,
+ createdAt: r.createdAt,
+ deals: parseDeals(r.dealsJson),
+ }));
+}
+
+export async function listDigestDates(): Promise<{ date: string; lastModified: Date }[]> {
+ const rows = await prisma.digestSnapshot.findMany({
+ select: { date: true, createdAt: true },
+ orderBy: { date: "desc" },
+ });
+ const byDate = new Map<string, Date>();
+ for (const r of rows) {
+ const prev = byDate.get(r.date);
+ if (!prev || r.createdAt > prev) byDate.set(r.date, r.createdAt);
+ }
+ return [...byDate.entries()].map(([date, lastModified]) => ({ date, lastModified }));
+}
diff --git a/src/lib/send-digest.ts b/src/lib/send-digest.ts
index 0cef9e8..87d6761 100644
--- a/src/lib/send-digest.ts
+++ b/src/lib/send-digest.ts
@@ -1,6 +1,7 @@
import { prisma } from "@/lib/db";
import { findHotDeals, type HotDeal } from "@/lib/hot-deals";
import { sendNewsletterEmail, newsletterSendMode } from "@/lib/newsletter";
+import { persistDigestSnapshot, currentSlot } from "@/lib/digest-snapshot";
// Digest send-to-list. Renders the same honesty-gated Top-10 the paid alerts
// use and fans it out to every CONFIRMED subscriber, each with their own
@@ -55,6 +56,11 @@ export interface DigestSendResult {
export async function sendDigestToSubscribers(opts: { baseUrl?: string; limit?: number } = {}): Promise<DigestSendResult> {
const baseUrl = opts.baseUrl || process.env.NEWSLETTER_BASE_URL || "http://localhost:3737";
const deals = await findHotDeals({ limit: opts.limit ?? 10 });
+
+ // Freeze this edition for the public /deals archive — even if every send
+ // below fails, the archive records what the digest showed at send time.
+ await persistDigestSnapshot(currentSlot(), deals);
+
const subscribers = await prisma.subscriber.findMany({ where: { status: "CONFIRMED" } });
let sent = 0;
← 8d98349 Contrarian fixes: digest send-to-list engine (TEST-logged),
·
back to Govarbitrage
·
Public SEO deal archive: /deals index + /deals/[date] teaser 5e22c50 →