-
Recent Matches
-
+{/if}
diff --git a/src/routes/(app)/admin/+page.server.ts b/src/routes/(app)/admin/+page.server.ts
new file mode 100644
index 0000000..c501d54
--- /dev/null
+++ b/src/routes/(app)/admin/+page.server.ts
@@ -0,0 +1,61 @@
+// src/routes/(app)/admin/+page.server.ts
+import { db } from '$lib/server/db';
+import { user } from '$lib/server/db/auth.schema';
+import { playerRating } from '$lib/server/db/schema';
+import { eq, desc } from 'drizzle-orm';
+import { redirect, error } from '@sveltejs/kit';
+import { requireRole, setUserRole } from '$lib/server/permissions';
+import type { PageServerLoad, Actions } from './$types';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ requireRole(locals, 'admin');
+
+ const users = await db.query.user.findMany({
+ orderBy: desc(user.createdAt)
+ });
+
+ // Fetch ratings for all users
+ const usersWithRatings = await Promise.all(
+ users.map(async (u) => {
+ const rating = await db.query.playerRating.findFirst({
+ where: eq(playerRating.userId, u.id)
+ });
+ return {
+ id: u.id,
+ name: u.name,
+ email: u.email,
+ image: u.image,
+ role: u.role ?? 'player',
+ createdAt: u.createdAt,
+ elo: rating?.elo ?? 1000,
+ wins: rating?.wins ?? 0,
+ losses: rating?.losses ?? 0
+ };
+ })
+ );
+
+ return { users: usersWithRatings };
+};
+
+export const actions: Actions = {
+ setRole: async ({ request, locals }) => {
+ requireRole(locals, 'admin');
+
+ const form = await request.formData();
+ const userId = form.get('userId')?.toString();
+ const role = form.get('role')?.toString();
+
+ if (!userId || !role) return { error: 'User ID and role are required' };
+ if (!['player', 'referee', 'admin'].includes(role)) {
+ return { error: 'Invalid role' };
+ }
+
+ // Prevent demoting yourself
+ if (userId === locals.user!.id && role !== 'admin') {
+ return { error: "You can't demote yourself" };
+ }
+
+ await setUserRole(userId, role as any);
+ return { success: true };
+ }
+};
diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte
new file mode 100644
index 0000000..1658847
--- /dev/null
+++ b/src/routes/(app)/admin/+page.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
Admin Panel
+
+ {data.users.length} registered user{data.users.length !== 1 ? 's' : ''}
+
+
+
+ {#if actionError}
+
{actionError}
+ {/if}
+
+
+
+
+ Avatar
+ User
+ ELO
+ W/L
+ Role
+
+
+ {#each data.users as u}
+ {@const badge = roleBadge[u.role] ?? roleBadge.player}
+
+
+ {#if u.image}
+

+ {:else}
+
+ {u.name.charAt(0).toUpperCase()}
+
+ {/if}
+
+
+
+
+
+
{u.elo}
+
+
+
+ {u.wins}W /
+ {u.losses}L
+
+
+
+
+
+ {/each}
+
+
diff --git a/src/routes/(app)/matches/+page.svelte b/src/routes/(app)/matches/+page.svelte
index f45faec..f7212ad 100644
--- a/src/routes/(app)/matches/+page.svelte
+++ b/src/routes/(app)/matches/+page.svelte
@@ -67,9 +67,11 @@
action="?/createMatch"
use:enhance={() => {
createError = '';
- return async ({ result }) => {
+ return async ({ result, update }) => {
if (result.type === 'failure' || (result.type === 'success' && (result.data as any)?.error)) {
createError = (result.data as any)?.error ?? 'Failed to create match';
+ } else {
+ await update();
}
};
}}
diff --git a/src/routes/(app)/matches/[id]/+page.server.ts b/src/routes/(app)/matches/[id]/+page.server.ts
index f69cdaf..6abbb7f 100644
--- a/src/routes/(app)/matches/[id]/+page.server.ts
+++ b/src/routes/(app)/matches/[id]/+page.server.ts
@@ -1,9 +1,13 @@
+import { user } from '$lib/server/db/schema';
+import { db } from '$lib/server/db';
import { getMatchFull, submitRoll, pickMap, cancelMatch } from '$lib/server/match/engine';
import { playPickedMap, closeLobby, forceStartGame } from '$lib/server/match/orchestrator';
import { getLobby } from '$lib/server/bancho/client';
import { error, redirect } from '@sveltejs/kit';
import { getBeatmap } from '$lib/server/osu/api';
import type { PageServerLoad, Actions } from './$types';
+import { eq } from 'drizzle-orm';
+
export const load: PageServerLoad = async ({ params, locals }) => {
if (!locals.user) redirect(302, '/');
@@ -41,6 +45,28 @@ export const load: PageServerLoad = async ({ params, locals }) => {
};
export const actions: Actions = {
+ reinvite: async ({ params, locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ const lobby = getLobby(params.id);
+ if (!lobby) return { error: 'No active IRC lobby for this match' };
+
+ const m = await getMatchFull(params.id);
+
+ const invited: string[] = [];
+ for (const p of m.participants) {
+ for (const pl of p.players) {
+ const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) });
+ if (u?.name) {
+ await lobby.invite(u.name);
+ invited.push(u.name);
+ }
+ }
+ }
+
+ return { reinvited: invited };
+ },
+
roll: async ({ params, locals }) => {
if (!locals.user) redirect(302, '/');
diff --git a/src/routes/(app)/matches/[id]/+page.svelte b/src/routes/(app)/matches/[id]/+page.svelte
index 76aabc0..28057a6 100644
--- a/src/routes/(app)/matches/[id]/+page.svelte
+++ b/src/routes/(app)/matches/[id]/+page.svelte
@@ -136,6 +136,27 @@
{/if}
+
+ {#if ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING'].includes(m.state)}
+
diff --git a/src/routes/(app)/settings/+page.server.ts b/src/routes/(app)/settings/+page.server.ts
new file mode 100644
index 0000000..d3788ca
--- /dev/null
+++ b/src/routes/(app)/settings/+page.server.ts
@@ -0,0 +1,46 @@
+// src/routes/(app)/settings/+page.server.ts
+import { redirect } from '@sveltejs/kit';
+import { auth } from '$lib/server/auth';
+import { db } from '$lib/server/db';
+import { playerRating } from '$lib/server/db/schema';
+import { account } from '$lib/server/db/auth.schema';
+import { eq, and } from 'drizzle-orm';
+import type { PageServerLoad, Actions } from './$types';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ if (!locals.user) redirect(302, '/');
+
+ // Get player rating
+ const rating = await db.query.playerRating.findFirst({
+ where: eq(playerRating.userId, locals.user.id)
+ });
+
+ // Get linked osu! account info
+ const osuAccount = await db.query.account.findFirst({
+ where: and(eq(account.userId, locals.user.id), eq(account.providerId, 'osu'))
+ });
+
+ return {
+ profile: {
+ id: locals.user.id,
+ name: locals.user.name,
+ email: locals.user.email,
+ image: locals.user.image,
+ role: locals.user.role ?? 'player',
+ createdAt: locals.user.createdAt
+ },
+ rating: rating
+ ? { elo: rating.elo, wins: rating.wins, losses: rating.losses }
+ : { elo: 1000, wins: 0, losses: 0 },
+ hasOsuLinked: !!osuAccount
+ };
+};
+
+export const actions: Actions = {
+ logout: async (event) => {
+ await auth.api.signOut({
+ headers: event.request.headers
+ });
+ redirect(302, '/');
+ }
+};
diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte
new file mode 100644
index 0000000..bd80675
--- /dev/null
+++ b/src/routes/(app)/settings/+page.svelte
@@ -0,0 +1,114 @@
+
+
+
+
Settings
+
Your profile and account settings
+
+
+
+
+ {#if data.profile.image}
+

+ {:else}
+
+ {data.profile.name.charAt(0).toUpperCase()}
+
+ {/if}
+
+
+
{data.profile.name}
+
+ {badge.label}
+
+
+
{data.profile.email}
+
+
+
+
+
+
Connected Accounts
+
+
🎮
+
+
osu!
+
+ {#if data.hasOsuLinked}
+ Connected as {data.profile.name}
+ {:else}
+ Not connected
+ {/if}
+
+
+ {#if data.hasOsuLinked}
+
Linked
+ {:else}
+
+ Connect
+
+ {/if}
+
+
+
+
+
+
+
Ranked Stats
+
+
+
{data.rating.elo}
+
ELO Rating
+
+
+
{data.rating.wins}
+
Wins
+
+
+
{data.rating.losses}
+
Losses
+
+
+
{winRate}{winRate !== '—' ? '%' : ''}
+
Win Rate
+
+
+
+
+
+
+
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte
new file mode 100644
index 0000000..b30a3e6
--- /dev/null
+++ b/src/routes/+error.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
{page.status}
+
+ {#if page.status === 404}
+ Page not found
+ {:else if page.status === 403}
+ Access denied
+ {:else if page.status === 401}
+ Not logged in
+ {:else}
+ Something went wrong
+ {/if}
+
+
+ {page.error?.message ?? 'An unexpected error occurred.'}
+
+
+ Back to Dashboard
+
+
+
diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts
new file mode 100644
index 0000000..de0e629
--- /dev/null
+++ b/src/routes/api/health/+server.ts
@@ -0,0 +1,27 @@
+// src/routes/api/health/+server.ts
+import { json } from '@sveltejs/kit';
+import { db } from '$lib/server/db';
+import { sql } from 'drizzle-orm';
+import type { RequestHandler } from './$types';
+
+export const GET: RequestHandler = async () => {
+ try {
+ // Check DB connectivity
+ await db.execute(sql`SELECT 1`);
+
+ return json({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime()
+ });
+ } catch (e: any) {
+ return json(
+ {
+ status: 'error',
+ error: 'Database connection failed',
+ timestamp: new Date().toISOString()
+ },
+ { status: 503 }
+ );
+ }
+};
diff --git a/src/routes/api/matches/+server.ts b/src/routes/api/matches/+server.ts
index abf3e5e..2069e3f 100644
--- a/src/routes/api/matches/+server.ts
+++ b/src/routes/api/matches/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/+server.ts
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { createMatch } from '$lib/server/match/engine';
@@ -35,7 +36,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
createdBy: locals.user.id
});
return json(result, { status: 201 });
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/+server.ts b/src/routes/api/matches/[id]/+server.ts
index b712ae4..3d9709f 100644
--- a/src/routes/api/matches/[id]/+server.ts
+++ b/src/routes/api/matches/[id]/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/+server.ts
import { json, error } from '@sveltejs/kit';
import { getMatchFull, cancelMatch } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -8,7 +9,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
try {
const m = await getMatchFull(params.id);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(404, e.message);
}
};
@@ -19,7 +20,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
try {
const m = await cancelMatch(params.id);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/lobby/+server.ts b/src/routes/api/matches/[id]/lobby/+server.ts
index bcd4c93..235fb62 100644
--- a/src/routes/api/matches/[id]/lobby/+server.ts
+++ b/src/routes/api/matches/[id]/lobby/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/lobby/+server.ts
import { json, error } from '@sveltejs/kit';
import { moveToLobby } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -10,7 +11,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
try {
const m = await moveToLobby(params.id, body.osuLobbyId);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/pick/+server.ts b/src/routes/api/matches/[id]/pick/+server.ts
index 808679f..dc13ac7 100644
--- a/src/routes/api/matches/[id]/pick/+server.ts
+++ b/src/routes/api/matches/[id]/pick/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/pick/+server.ts
import { json, error } from '@sveltejs/kit';
import { pickMap } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -13,7 +14,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
try {
const game = await pickMap(params.id, participantId, mappoolSlotId);
return json(game);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/roll/+server.ts b/src/routes/api/matches/[id]/roll/+server.ts
index d2bf742..d955bba 100644
--- a/src/routes/api/matches/[id]/roll/+server.ts
+++ b/src/routes/api/matches/[id]/roll/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/roll/+server.ts
import { json, error } from '@sveltejs/kit';
import { submitRoll } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -13,7 +14,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
try {
const m = await submitRoll(params.id, participantId, value);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/rolling/+server.ts b/src/routes/api/matches/[id]/rolling/+server.ts
index b3b538e..f0cf605 100644
--- a/src/routes/api/matches/[id]/rolling/+server.ts
+++ b/src/routes/api/matches/[id]/rolling/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/rolling/+server.ts
import { json, error } from '@sveltejs/kit';
import { moveToRolling } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -8,7 +9,7 @@ export const POST: RequestHandler = async ({ params, locals }) => {
try {
const m = await moveToRolling(params.id);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/matches/[id]/score/+server.ts b/src/routes/api/matches/[id]/score/+server.ts
index 8ceafe1..0525002 100644
--- a/src/routes/api/matches/[id]/score/+server.ts
+++ b/src/routes/api/matches/[id]/score/+server.ts
@@ -1,3 +1,4 @@
+// src/routes/api/matches/[id]/score/+server.ts
import { json, error } from '@sveltejs/kit';
import { submitGameScores } from '$lib/server/match/engine';
import type { RequestHandler } from './$types';
@@ -13,7 +14,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
try {
const m = await submitGameScores(matchGameId, scores);
return json(m);
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/api/queue/+server.ts b/src/routes/api/queue/+server.ts
index 6715e2e..a4d65c6 100644
--- a/src/routes/api/queue/+server.ts
+++ b/src/routes/api/queue/+server.ts
@@ -1,8 +1,9 @@
+// src/routes/api/queue/+server.ts
import { json, error } from '@sveltejs/kit';
import { joinQueue, leaveQueue, getQueueStatus } from '$lib/server/match/engine';
import { db } from '$lib/server/db';
-import { team, teamMember } from '$lib/server/db/schema';
-import { eq, and } from 'drizzle-orm';
+import { teamMember } from '$lib/server/db/schema';
+import { eq } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
@@ -29,7 +30,7 @@ export const POST: RequestHandler = async ({ locals }) => {
try {
const result = await joinQueue(locals.user.id, personalTeam.id);
return json({ matched: !!result, match: result });
- } catch (e) {
+ } catch (e: any) {
error(400, e.message);
}
};
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
new file mode 100644
index 0000000..063dc18
--- /dev/null
+++ b/src/routes/login/+page.server.ts
@@ -0,0 +1,18 @@
+import { redirect } from '@sveltejs/kit';
+import { auth } from '$lib/server/auth';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async (event) => {
+ if (event.locals.user) redirect(302, '/');
+
+ const result = await auth.api.signInSocial({
+ body: {
+ provider: 'osu',
+ callbackURL: '/'
+ }
+ });
+
+ if (result.url) redirect(302, result.url);
+
+ return {};
+};