Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app/api/cron/backfill/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ async function processStorylineCreated(
throw new Error(`Database error (genesis plot): ${plotError.message}`);
}

// Reconcile plot_count from actual plots rows (prevents genesis double-count)
await reconcileStorylinePlotCount(supabase, Number(storylineId));

return { genesisPlotFailed: false };
}

Expand Down
58 changes: 58 additions & 0 deletions src/app/api/cron/reconcile-plots/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextResponse } from "next/server";
import { createServerClient } from "../../../../../lib/supabase";
import { reconcileStorylinePlotCount } from "../../../../../lib/reconcile";
import { STORY_FACTORY } from "../../../../../lib/contracts/constants";

/**
* One-time reconciliation endpoint: re-counts plot_count for ALL storylines
* from the plots table. Safe to run multiple times (idempotent).
*
* POST /api/cron/reconcile-plots
*/
/** Cron authorization — fail closed in production when CRON_SECRET is unset */
function verifyCron(req: Request): boolean {
const secret = process.env.CRON_SECRET;
if (!secret) {
return process.env.NODE_ENV !== "production";
}
const authHeader = req.headers.get("authorization");
return authHeader === `Bearer ${secret}`;
}

export async function POST(req: Request) {
if (!verifyCron(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const supabase = createServerClient();
if (!supabase) {
return NextResponse.json({ error: "Supabase not configured" }, { status: 500 });
}

// Fetch all storyline IDs
const { data: storylines, error: fetchError } = await supabase
.from("storylines")
.select("storyline_id")
.eq("contract_address", STORY_FACTORY.toLowerCase());

if (fetchError) {
return NextResponse.json({ error: fetchError.message }, { status: 500 });
}

if (!storylines || storylines.length === 0) {
return NextResponse.json({ reconciled: 0 });
}

let reconciled = 0;
const errors: string[] = [];

for (const s of storylines) {
try {
await reconcileStorylinePlotCount(supabase, s.storyline_id);
reconciled++;
} catch (err) {
errors.push(`storyline ${s.storyline_id}: ${err instanceof Error ? err.message : String(err)}`);
}
}

return NextResponse.json({ reconciled, total: storylines.length, errors });
}
4 changes: 4 additions & 0 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { type Hex, decodeEventLog, encodeEventTopics } from "viem";
import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc";

Check warning on line 3 in src/app/api/index/storyline/route.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'getReceiptWithRetry' is defined but never used
import { createServerClient } from "../../../../../lib/supabase";
import { validateRecentTx } from "../../../../../lib/index-auth";
import {
Expand All @@ -12,6 +12,7 @@
import { hashContent } from "../../../../../lib/content";
import { GENRES, LANGUAGES } from "../../../../../lib/genres";
import type { Database } from "../../../../../lib/supabase";
import { reconcileStorylinePlotCount } from "../../../../../lib/reconcile";

const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/";
const IPFS_TIMEOUT_MS = 10_000;
Expand Down Expand Up @@ -179,5 +180,8 @@
return error(`Database error (genesis plot): ${plotDbError.message}`, 500);
}

// Reconcile plot_count from actual plots rows (prevents genesis double-count)
await reconcileStorylinePlotCount(supabase, Number(storylineId));

return NextResponse.json({ success: true });
}
Loading