New scheduling UI!

This commit is contained in:
George Powell
2026-07-07 17:02:24 -04:00
parent be0d7ad297
commit 21efdd4eef
19 changed files with 2127 additions and 39 deletions
+89
View File
@@ -0,0 +1,89 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { dailyCompletions } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import {
scheduleSubmission,
validateSelection,
getCooldownState
} from '$lib/server/verse-submission';
/**
* POST /api/submit-verse
*
* Accepts a user-chosen verse (bookId/chapter/verse) and reserves a concrete
* future date for it via the scheduling scan. Auth required, solved-today gate,
* 7-day rolling cooldown, and structural validation all enforced here.
*
* Body: { bookId, chapter, verse, localDate }
*/
export const POST: RequestHandler = async ({ request, locals }) => {
// 1. Auth.
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = locals.user.id;
let body: any;
try {
body = await request.json();
} catch {
return json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { bookId, chapter, verse, localDate } = body ?? {};
// 2. Solved-today gate (engagement gate, not security).
if (typeof localDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(localDate)) {
return json({ error: 'A valid localDate (YYYY-MM-DD) is required' }, { status: 400 });
}
const [completion] = await db
.select({ id: dailyCompletions.id })
.from(dailyCompletions)
.where(
and(
eq(dailyCompletions.anonymousId, userId),
eq(dailyCompletions.date, localDate)
)
)
.limit(1);
if (!completion) {
return json({ error: "Solve today's puzzle first" }, { status: 403 });
}
// 3. Cooldown (rolling 7×24h, server UTC).
const cooldown = await getCooldownState(db, userId);
if (cooldown.onCooldown) {
return json(
{ error: 'Cooldown active', cooldownEndsAt: cooldown.cooldownEndsAt },
{ status: 429 }
);
}
// 4. Structural validation.
const validationError = validateSelection({ bookId, chapter, verse });
if (validationError) {
return json({ error: validationError }, { status: 400 });
}
// 57. Compute window + scheduling scan + transactional insert (with retry).
try {
const result = await scheduleSubmission(
db,
{ bookId, chapter, verse },
userId
);
return json(
{
scheduledDate: result.scheduledDate,
reference: result.reference,
windowText: result.windowText
},
{ status: 201 }
);
} catch (err) {
console.error('submit-verse failed:', err);
return json({ error: 'Failed to schedule submission' }, { status: 500 });
}
};