From 7c4c8eecf7199d722400683c03c8bef8feddb6f2 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 3 Apr 2026 14:41:04 +0100 Subject: [PATCH] [#808] Fix deadline enforcement: check has_deadline, use block timestamp Critical: deadline logic was incorrectly blocking storylines without deadlines (has_deadline=false). 1. AddPlotButton: skip deadline check when hasDeadline is false 2. Create page: isStorylineExpired checks has_deadline before time check 3. API indexer: select has_deadline, skip check when false; use block timestamp instead of Date.now() to avoid rejecting plots the contract already accepted near the deadline edge Fixes #808 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/index/plot/route.ts | 9 ++++++--- src/app/create/page.tsx | 1 + src/app/story/[storylineId]/page.tsx | 2 +- src/components/AddPlotButton.tsx | 8 +++++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index 0653d4d7..b782a684 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -117,7 +117,7 @@ export async function POST(req: Request) { const DEADLINE_MS = 168 * 60 * 60 * 1000; // 7 days — matches DEADLINE_HOURS in DeadlineCountdown const { data: storylineRow } = await supabase .from("storylines") - .select("last_plot_time, sunset") + .select("last_plot_time, sunset, has_deadline") .eq("storyline_id", Number(storylineId)) .single(); @@ -125,9 +125,12 @@ export async function POST(req: Request) { if (storylineRow.sunset) { return error("Storyline has sunset — no new plots allowed", 400); } - if (storylineRow.last_plot_time) { + if (storylineRow.has_deadline && storylineRow.last_plot_time) { const deadline = new Date(storylineRow.last_plot_time).getTime() + DEADLINE_MS; - if (Date.now() > deadline) { + // Use block timestamp (when tx was mined) instead of Date.now() to avoid + // rejecting plots that the contract already accepted near the deadline edge + const blockTimeMs = Number(blockTimestamp) * 1000; + if (blockTimeMs > deadline) { return error("Storyline deadline expired — no new plots allowed", 400); } } diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx index b5450d28..af67ec03 100644 --- a/src/app/create/page.tsx +++ b/src/app/create/page.tsx @@ -65,6 +65,7 @@ async function fetchWriterStorylines(address: string): Promise { function isStorylineExpired(s: Storyline): boolean { if (s.sunset) return true; + if (!s.has_deadline) return false; if (!s.last_plot_time) return false; return Date.now() > new Date(s.last_plot_time).getTime() + DEADLINE_MS; } diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index 6d808923..3f80dead 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -372,7 +372,7 @@ function StoryHeader({ )} - + ); } diff --git a/src/components/AddPlotButton.tsx b/src/components/AddPlotButton.tsx index fb2c6fa8..516bbb42 100644 --- a/src/components/AddPlotButton.tsx +++ b/src/components/AddPlotButton.tsx @@ -15,25 +15,27 @@ export function AddPlotButton({ writerAddress, lastPlotTime, sunset, + hasDeadline, }: { storylineId: number; writerAddress: string; lastPlotTime?: string | null; sunset?: boolean; + hasDeadline?: boolean; }) { const { address } = useAccount(); if (!address || address.toLowerCase() !== writerAddress.toLowerCase()) return null; - const expired = sunset || (lastPlotTime ? isDeadlineExpired(lastPlotTime) : false); + const expired = sunset || (hasDeadline !== false && lastPlotTime ? isDeadlineExpired(lastPlotTime) : false); if (expired) { return (
- Deadline expired + {sunset ? "Story complete" : "Deadline expired"}
); }