From c8f96442eb4f67edab3e6739b7521f7c44deccd0 Mon Sep 17 00:00:00 2001 From: Monster0506 Date: Tue, 2 Jun 2026 22:16:12 -0400 Subject: [PATCH 1/2] pass env vars and stop early auth rejection --- docker-compose.yml | 2 ++ src/hooks.server.ts | 4 +++- src/routes/admin/+layout.server.ts | 14 ++------------ 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fe6b5e9..4ee4e40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,8 @@ services: BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID} DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET} + DISCORD_GUILD_ID: ${DISCORD_GUILD_ID} + DISCORD_ADMIN_ROLE_ID: ${DISCORD_ADMIN_ROLE_ID} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} networks: diff --git a/src/hooks.server.ts b/src/hooks.server.ts index baa0788..8eb28ef 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,6 @@ import { error, redirect, type Handle } from '@sveltejs/kit'; import { auth } from '$lib/server/auth'; +import { refreshDiscordRole } from '$lib/server/admin'; export const handle: Handle = async ({ event, resolve }) => { const session = await auth.api.getSession({ headers: event.request.headers }); @@ -8,7 +9,8 @@ export const handle: Handle = async ({ event, resolve }) => { if (event.url.pathname.startsWith('/admin')) { if (!event.locals.user) throw redirect(302, '/login'); - if (event.locals.user.role !== 'admin') { + const role = await refreshDiscordRole(event.locals.user.id); + if (role !== 'admin') { throw error( 403, "Admin access required. If you should have access, sign out and back in via Discord (your token may predate the guilds.members.read scope), or set your role to 'admin' directly in the DB." diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts index 041f417..d70be27 100644 --- a/src/routes/admin/+layout.server.ts +++ b/src/routes/admin/+layout.server.ts @@ -1,17 +1,7 @@ -import { error, redirect } from '@sveltejs/kit'; -import { refreshDiscordRole } from '$lib/server/admin'; +import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(302, '/login'); - - const role = await refreshDiscordRole(locals.user.id); - if (role !== 'admin') { - throw error( - 403, - "Admin access required. If you should have access, sign out and back in via Discord (your token may predate the guilds.members.read scope), or set your role to 'admin' directly in the DB." - ); - } - - return { role }; + return { role: locals.user.role }; }; From 404978db44ef5825259cbe88373a8749558a9750 Mon Sep 17 00:00:00 2001 From: Monster0506 Date: Wed, 3 Jun 2026 07:33:21 -0400 Subject: [PATCH 2/2] generalize bingo card size to odd sized cards --- src/lib/bingo.ts | 58 +++++++++++++++----------- src/routes/admin/tiles/+page.server.ts | 19 +++++---- src/routes/admin/tiles/+page.svelte | 4 +- src/routes/bingo/+page.server.ts | 19 +++++---- src/routes/bingo/+page.svelte | 12 ++++-- 5 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/lib/bingo.ts b/src/lib/bingo.ts index 4345867..b05e119 100644 --- a/src/lib/bingo.ts +++ b/src/lib/bingo.ts @@ -1,42 +1,51 @@ -export const MAX_GRID_SIZE = 6; export const GRID_SIZE = 5; -if (GRID_SIZE > MAX_GRID_SIZE) { - throw new Error( - `GRID_SIZE (${GRID_SIZE}) exceeds MAX_GRID_SIZE (${MAX_GRID_SIZE}). Cards are capped at 6x6.` - ); +export function effectivePoolSize(tiles: { isFreeSpace: boolean }[]): number { + const extraFreeSpaces = Math.max(0, tiles.filter((t) => t.isFreeSpace).length - 1); + return tiles.length - extraFreeSpaces; } -// All 12 winning lines on a 5x5 board, expressed as tile positions (0..24). -export const WIN_LINES: number[][] = (() => { +export function getCardSize(tileCount: number): number { + let size = 5; + while ((size + 2) ** 2 <= tileCount) size += 2; + return size; +} + +export function getWinLines(gridSize: number): number[][] { const lines: number[][] = []; - for (let r = 0; r < GRID_SIZE; r++) { - lines.push(Array.from({ length: GRID_SIZE }, (_, c) => r * GRID_SIZE + c)); + for (let r = 0; r < gridSize; r++) { + lines.push(Array.from({ length: gridSize }, (_, c) => r * gridSize + c)); } - for (let c = 0; c < GRID_SIZE; c++) { - lines.push(Array.from({ length: GRID_SIZE }, (_, r) => r * GRID_SIZE + c)); + for (let c = 0; c < gridSize; c++) { + lines.push(Array.from({ length: gridSize }, (_, r) => r * gridSize + c)); } - lines.push(Array.from({ length: GRID_SIZE }, (_, i) => i * GRID_SIZE + i)); - lines.push(Array.from({ length: GRID_SIZE }, (_, i) => i * GRID_SIZE + (GRID_SIZE - 1 - i))); + lines.push(Array.from({ length: gridSize }, (_, i) => i * gridSize + i)); + lines.push(Array.from({ length: gridSize }, (_, i) => i * gridSize + (gridSize - 1 - i))); return lines; -})(); +} + +export const WIN_LINES = getWinLines(GRID_SIZE); -export function detectBingo(completedPositions: Set): { +export function detectBingo( + completedPositions: Set, + gridSize: number = GRID_SIZE +): { hasBingo: boolean; winningLines: number[][]; winningPositions: Set; } { - const winningLines = WIN_LINES.filter((line) => line.every((p) => completedPositions.has(p))); + const winLines = getWinLines(gridSize); + const winningLines = winLines.filter((line) => line.every((p) => completedPositions.has(p))); const winningPositions = new Set(winningLines.flat()); return { hasBingo: winningLines.length > 0, winningLines, winningPositions }; } -export function describeWinLine(line: number[]): string { - const rows = line.map((p) => Math.floor(p / GRID_SIZE)); - const cols = line.map((p) => p % GRID_SIZE); +export function describeWinLine(line: number[], gridSize: number = GRID_SIZE): string { + const rows = line.map((p) => Math.floor(p / gridSize)); + const cols = line.map((p) => p % gridSize); if (rows.every((r) => r === rows[0])) return `Row ${rows[0] + 1}`; if (cols.every((c) => c === cols[0])) return `Column ${cols[0] + 1}`; - if (line.every((p, i) => p === i * GRID_SIZE + i)) return 'Diagonal ↘'; + if (line.every((p, i) => p === i * gridSize + i)) return 'Diagonal ↘'; return 'Diagonal ↗'; } @@ -47,14 +56,15 @@ export function describeWinLine(line: number[]): string { */ export function bingoWinTransition( before: Set, - after: Set + after: Set, + gridSize: number = GRID_SIZE ): { justWon: boolean; lineLabel: string | null } { - const beforeRes = detectBingo(before); - const afterRes = detectBingo(after); + const beforeRes = detectBingo(before, gridSize); + const afterRes = detectBingo(after, gridSize); if (!afterRes.hasBingo || beforeRes.hasBingo) { return { justWon: false, lineLabel: null }; } const beforeKeys = new Set(beforeRes.winningLines.map((l) => l.join(','))); const newLine = afterRes.winningLines.find((l) => !beforeKeys.has(l.join(','))); - return { justWon: true, lineLabel: newLine ? describeWinLine(newLine) : null }; + return { justWon: true, lineLabel: newLine ? describeWinLine(newLine, gridSize) : null }; } diff --git a/src/routes/admin/tiles/+page.server.ts b/src/routes/admin/tiles/+page.server.ts index 5defbe4..6dd118c 100644 --- a/src/routes/admin/tiles/+page.server.ts +++ b/src/routes/admin/tiles/+page.server.ts @@ -3,22 +3,22 @@ import { eq, sql } from 'drizzle-orm'; import { randomUUID } from 'node:crypto'; import { db } from '$lib/server/db'; import { bingoTile } from '$lib/server/db/schema'; -import { GRID_SIZE } from '$lib/bingo'; +import { getCardSize, effectivePoolSize } from '$lib/bingo'; import { isAdmin } from '$lib/server/admin'; import { logActivity } from '$lib/server/activity'; import type { Actions, PageServerLoad } from './$types'; -const TARGET_TILES = GRID_SIZE * GRID_SIZE; - export const load: PageServerLoad = async () => { const tiles = await db .select({ id: bingoTile.id, label: bingoTile.label, position: bingoTile.position, isFreeSpace: bingoTile.isFreeSpace, isActive: bingoTile.isActive }) .from(bingoTile) .orderBy(bingoTile.position); + const gridSize = getCardSize(effectivePoolSize(tiles)); + const target = gridSize * gridSize; return { tiles, - target: TARGET_TILES, - gridSize: GRID_SIZE + target, + gridSize }; }; @@ -114,15 +114,16 @@ export const actions: Actions = { .select({ id: bingoTile.id, position: bingoTile.position }) .from(bingoTile); const total = existing.length + labels.length; + const minTarget = 25; - if (total < TARGET_TILES) { - const short = TARGET_TILES - total; + if (total < minTarget) { + const short = minTarget - total; return fail(400, { form: 'bulkAdd', - message: `Upload would result in ${total} tiles, short of the ${TARGET_TILES} (${GRID_SIZE}×${GRID_SIZE}) needed for a complete card. Add ${short} more row${short === 1 ? '' : 's'} to your CSV.`, + message: `Upload would result in ${total} tiles, short of the ${minTarget} needed for a complete card. Add ${short} more row${short === 1 ? '' : 's'} to your CSV.`, existing: existing.length, incoming: labels.length, - target: TARGET_TILES + target: minTarget }); } diff --git a/src/routes/admin/tiles/+page.svelte b/src/routes/admin/tiles/+page.svelte index b6e9a48..575e454 100644 --- a/src/routes/admin/tiles/+page.svelte +++ b/src/routes/admin/tiles/+page.svelte @@ -84,8 +84,8 @@

- One label per line. Will be appended to existing tiles. Total must equal exactly {data.target} - ({data.gridSize}×{data.gridSize}) after upload — partial cards are rejected. + One label per line. Will be appended to existing tiles. Total must reach at least 25 after + upload — partial cards are rejected.

{#if bulkError}

{bulkError}

diff --git a/src/routes/bingo/+page.server.ts b/src/routes/bingo/+page.server.ts index 5f5b7f3..05f44a4 100644 --- a/src/routes/bingo/+page.server.ts +++ b/src/routes/bingo/+page.server.ts @@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto'; import { db } from '$lib/server/db'; import { bingoProgress, bingoTile, user } from '$lib/server/db/schema'; import { sql } from 'drizzle-orm'; -import { detectBingo, bingoWinTransition } from '$lib/bingo'; +import { detectBingo, bingoWinTransition, getCardSize, effectivePoolSize } from '$lib/bingo'; import { shuffleTilesForUser } from '$lib/server/cardShuffle'; import type { Actions, PageServerLoad } from './$types'; import { logActivity } from '$lib/server/activity'; @@ -25,6 +25,7 @@ async function resetUserBoard(userId: string, regenerateSeed: boolean): Promise< async function boardPositions(userId: string): Promise<{ ordered: { id: string; isFreeSpace: boolean }[]; positions: Set; + cardSize: number; }> { const tiles = await db .select({ id: bingoTile.id, position: bingoTile.position, isFreeSpace: bingoTile.isFreeSpace }) @@ -40,12 +41,13 @@ async function boardPositions(userId: string): Promise<{ .from(bingoProgress) .where(eq(bingoProgress.userId, userId)); const completed = new Set(progress.map((p) => p.tileId)); - const ordered = shuffleTilesForUser(tiles, u?.cardSeed ?? null); + const cardSize = getCardSize(effectivePoolSize(tiles)); + const ordered = shuffleTilesForUser(tiles, u?.cardSeed ?? null, cardSize); const positions = new Set(); ordered.forEach((t, idx) => { if (completed.has(t.id) || t.isFreeSpace) positions.add(idx); }); - return { ordered, positions }; + return { ordered, positions, cardSize }; } async function ensureCardSeed(userId: string, existing: string | null): Promise { @@ -78,8 +80,9 @@ export const load: PageServerLoad = async ({ locals }) => { .where(eq(user.id, locals.user.id)) .limit(1); + const cardSize = getCardSize(effectivePoolSize(tiles)); const seed = await ensureCardSeed(locals.user.id, me?.cardSeed ?? null); - const ordered = shuffleTilesForUser(tiles, seed); + const ordered = shuffleTilesForUser(tiles, seed, cardSize); const completed = new Set(progress.map((p) => p.tileId)); @@ -87,7 +90,7 @@ export const load: PageServerLoad = async ({ locals }) => { ordered.forEach((t, idx) => { if (completed.has(t.id) || t.isFreeSpace) completedPositions.add(idx); }); - const { hasBingo, winningPositions } = detectBingo(completedPositions); + const { hasBingo, winningPositions } = detectBingo(completedPositions, cardSize); return { tiles: ordered.map((t, idx) => ({ @@ -96,6 +99,8 @@ export const load: PageServerLoad = async ({ locals }) => { winning: winningPositions.has(idx) })), hasBingo, + gridSize: cardSize, + tooFewTiles: tiles.length < 25, verifiedAt: me?.bingoVerifiedAt ?? null, verifiedBy: me?.bingoVerifiedBy ?? null }; @@ -140,12 +145,12 @@ export const actions: Actions = { await logActivity({ userId: locals.user.id, type: 'tile_complete', detail: tile.label }); - const { ordered, positions } = await boardPositions(locals.user.id); + const { ordered, positions, cardSize } = await boardPositions(locals.user.id); const toggledIdx = ordered.findIndex((t) => t.id === tileId); if (toggledIdx >= 0) { const before = new Set(positions); before.delete(toggledIdx); - const { justWon, lineLabel } = bingoWinTransition(before, positions); + const { justWon, lineLabel } = bingoWinTransition(before, positions, cardSize); if (justWon) { await logActivity({ userId: locals.user.id, type: 'bingo_win', detail: lineLabel }); } diff --git a/src/routes/bingo/+page.svelte b/src/routes/bingo/+page.svelte index b398d08..48e6a03 100644 --- a/src/routes/bingo/+page.svelte +++ b/src/routes/bingo/+page.svelte @@ -1,6 +1,5 @@