10 Commits

Author SHA1 Message Date
yohlo
63ea515a31 activity logging middleware 2025-09-30 10:47:02 -05:00
yohlo
8b1bbe213d test sse fixes 2025-09-29 21:35:38 -05:00
yohlo
ed538b7373 test sse fixes 2025-09-29 21:35:12 -05:00
yohlo
03e3bbcbc0 test sse fixes 2025-09-29 21:31:00 -05:00
yohlo
baf75eddba test sse fixes 2025-09-29 21:28:22 -05:00
yohlo
5094933302 update admin 2025-09-29 15:49:18 -05:00
yohlo
9564b46d45 quick fix 2025-09-29 15:42:00 -05:00
yohlo
ece5094f13 quick fix 2025-09-29 15:40:41 -05:00
yohlo
cfe1ee7171 passwordless fix 2025-09-29 15:14:41 -05:00
yohlo
3a41609a91 bug fixes, layout fixes 2025-09-29 15:13:41 -05:00
20 changed files with 418 additions and 48 deletions

View File

@@ -0,0 +1,108 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "json4225120046",
"maxSize": 0,
"name": "arguments",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"cascadeDelete": false,
"collectionId": "pbc_3072146508",
"hidden": false,
"id": "relation2551806565",
"maxSelect": 1,
"minSelect": 0,
"name": "player",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3293145029",
"max": 0,
"min": 0,
"name": "user_agent",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_1262591861",
"indexes": [],
"listRule": null,
"name": "activities",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861");
return app.delete(collection);
})

View File

@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// add field
collection.fields.addAt(5, new Field({
"hidden": false,
"id": "number2254405824",
"max": null,
"min": null,
"name": "duration",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// remove field
collection.fields.removeById("number2254405824")
return app.save(collection)
})

View File

@@ -0,0 +1,43 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// add field
collection.fields.addAt(6, new Field({
"hidden": false,
"id": "bool1862328242",
"name": "success",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}))
// add field
collection.fields.addAt(7, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1574812785",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1262591861")
// remove field
collection.fields.removeById("bool1862328242")
// remove field
collection.fields.removeById("text1574812785")
return app.save(collection)
})

View File

@@ -330,6 +330,8 @@ async function startServer() {
const server = Bun.serve({ const server = Bun.serve({
port: PORT, port: PORT,
idleTimeout: 255,
routes: { routes: {
// Serve static assets (preloaded or on-demand) // Serve static assets (preloaded or on-demand)
...routes, ...routes,

View File

@@ -37,7 +37,7 @@ export const Route = createRootRouteWithContext<{
{ {
name: "viewport", name: "viewport",
content: content:
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=overlays-content", "width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=resizes-content",
}, },
], ],
links: [ links: [
@@ -122,8 +122,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
{...mantineHtmlProps} {...mantineHtmlProps}
style={{ style={{
overflowX: "hidden", overflowX: "hidden",
overflowY: "hidden", height: "100%",
position: "fixed",
width: "100%", width: "100%",
}} }}
> >
@@ -135,9 +134,10 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<body <body
style={{ style={{
overflowX: "hidden", overflowX: "hidden",
overflowY: "hidden", height: "100%",
position: "fixed",
width: "100%", width: "100%",
margin: 0,
padding: 0,
}} }}
> >
<div className="app">{children}</div> <div className="app">{children}</div>

View File

@@ -3,11 +3,16 @@ import { serverEvents, type ServerEvent } from "@/lib/events/emitter";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import { superTokensRequestMiddleware } from "@/utils/supertokens"; import { superTokensRequestMiddleware } from "@/utils/supertokens";
let activeConnections = 0;
export const Route = createFileRoute("/api/events/$")({ export const Route = createFileRoute("/api/events/$")({
server: { server: {
middleware: [superTokensRequestMiddleware], middleware: [superTokensRequestMiddleware],
handlers: { handlers: {
GET: ({ request, context }) => { GET: ({ request }) => {
activeConnections++;
const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
logger.info(`ServerEvents | New connection ${connectionId}. Active: ${activeConnections}`);
const stream = new ReadableStream({ const stream = new ReadableStream({
start(controller) { start(controller) {
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`; const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
@@ -17,6 +22,10 @@ export const Route = createFileRoute("/api/events/$")({
logger.info("ServerEvents | Event received", event); logger.info("ServerEvents | Event received", event);
const message = `data: ${JSON.stringify(event)}\n\n`; const message = `data: ${JSON.stringify(event)}\n\n`;
try { try {
if (!controller.desiredSize || controller.desiredSize <= 0) {
logger.warn("ServerEvents | Stream closed, skipping event");
return;
}
controller.enqueue(new TextEncoder().encode(message)); controller.enqueue(new TextEncoder().encode(message));
} catch (error) { } catch (error) {
logger.error("ServerEvents | Error sending SSE message", error); logger.error("ServerEvents | Error sending SSE message", error);
@@ -29,16 +38,34 @@ export const Route = createFileRoute("/api/events/$")({
const pingInterval = setInterval(() => { const pingInterval = setInterval(() => {
try { try {
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`; if (!controller.desiredSize || controller.desiredSize <= 0) {
clearInterval(pingInterval);
return;
}
const pingMessage = `data: ${JSON.stringify({ type: "ping", timestamp: Date.now() })}\n\n`;
controller.enqueue(new TextEncoder().encode(pingMessage)); controller.enqueue(new TextEncoder().encode(pingMessage));
} catch (e) { } catch (e) {
logger.error("ServerEvents | Ping interval error", e);
clearInterval(pingInterval); clearInterval(pingInterval);
} }
}, 30000); }, 15000);
setTimeout(() => {
try {
const heartbeatMessage = `data: ${JSON.stringify({ type: "heartbeat", timestamp: Date.now() })}\n\n`;
controller.enqueue(new TextEncoder().encode(heartbeatMessage));
} catch (e) {
logger.error("ServerEvents | Heartbeat error", e);
}
}, 1000);
const cleanup = () => { const cleanup = () => {
activeConnections--;
serverEvents.off("test", handleEvent); serverEvents.off("test", handleEvent);
serverEvents.off("match", handleEvent);
serverEvents.off("reaction", handleEvent);
clearInterval(pingInterval); clearInterval(pingInterval);
logger.info(`ServerEvents | Connection ${connectionId} cleanup completed. Active: ${activeConnections}`);
}; };
request.signal?.addEventListener("abort", cleanup); request.signal?.addEventListener("abort", cleanup);
@@ -49,10 +76,14 @@ export const Route = createFileRoute("/api/events/$")({
return new Response(stream, { return new Response(stream, {
headers: { headers: {
"Content-Type": "text/event-stream", "Content-Type": "text/event-stream",
"Cache-Control": "no-cache", "Cache-Control": "no-cache, no-store, must-revalidate",
Connection: "keep-alive", "Connection": "keep-alive",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control", "Access-Control-Allow-Headers": "Cache-Control",
"X-Accel-Buffering": "no",
"X-Proxy-Buffering": "no",
"Proxy-Buffering": "off",
"Transfer-Encoding": "chunked",
}, },
}); });
}, },

View File

@@ -31,7 +31,11 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
pos='relative' pos='relative'
h='100dvh' h='100dvh'
mah='100dvh' mah='100dvh'
// style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }} style={{
height: `${viewport.height}px`,
minHeight: '100dvh',
// top: viewport.top
}}
> >
<Header {...header} /> <Header {...header} />
<AppShell.Main <AppShell.Main

View File

@@ -19,7 +19,7 @@ const Navbar = () => {
// boxShadow: `5px 5px ${boxShadowColor}`, borderColor // boxShadow: `5px 5px ${boxShadowColor}`, borderColor
if (isMobile) return ( if (isMobile) return (
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1.5rem)' pos='fixed' m='0.75rem' bottom='0' style={{ zIndex: 10 }}> <Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom='0' style={{ zIndex: 10 }}>
<Group gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}> <Group gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
{links.map((link) => ( {links.map((link) => (
<NavLink key={link.href} {...link} /> <NavLink key={link.href} {...link} />

View File

@@ -19,11 +19,17 @@ const useVisualViewportSize = () => {
useEffect(() => { useEffect(() => {
if (!windowExists) return; if (!windowExists) return;
setSize();
window.visualViewport?.addEventListener('resize', setSize, eventListerOptions); window.visualViewport?.addEventListener('resize', setSize, eventListerOptions);
window.visualViewport?.addEventListener('scroll', setSize, eventListerOptions);
return () => { return () => {
window.visualViewport?.removeEventListener('resize', setSize); window.visualViewport?.removeEventListener('resize', setSize);
window.visualViewport?.removeEventListener('scroll', setSize);
} }
}, []); }, [setSize]);
return windowSize; return windowSize;
} }

View File

@@ -9,6 +9,7 @@ import { MatchInput } from "@/features/matches/types";
import { serverEvents } from "@/lib/events/emitter"; import { serverEvents } from "@/lib/events/emitter";
import { superTokensFunctionMiddleware } from "@/utils/supertokens"; import { superTokensFunctionMiddleware } from "@/utils/supertokens";
import { PlayerInfo } from "../players/types"; import { PlayerInfo } from "../players/types";
import { serverFnLoggingMiddleware } from "@/utils/activities";
const orderedTeamsSchema = z.object({ const orderedTeamsSchema = z.object({
tournamentId: z.string(), tournamentId: z.string(),
@@ -17,7 +18,7 @@ const orderedTeamsSchema = z.object({
export const generateTournamentBracket = createServerFn() export const generateTournamentBracket = createServerFn()
.inputValidator(orderedTeamsSchema) .inputValidator(orderedTeamsSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, orderedTeamIds } }) => .handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
toServerResult(async () => { toServerResult(async () => {
logger.info("Generating tournament bracket", { logger.info("Generating tournament bracket", {
@@ -138,7 +139,7 @@ export const generateTournamentBracket = createServerFn()
export const startMatch = createServerFn() export const startMatch = createServerFn()
.inputValidator(z.string()) .inputValidator(z.string())
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult(async () => { toServerResult(async () => {
logger.info("Starting match", data); logger.info("Starting match", data);
@@ -171,7 +172,7 @@ const endMatchSchema = z.object({
}); });
export const endMatch = createServerFn() export const endMatch = createServerFn()
.inputValidator(endMatchSchema) .inputValidator(endMatchSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) => .handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
toServerResult(async () => { toServerResult(async () => {
logger.info("Ending match", matchId); logger.info("Ending match", matchId);

View File

@@ -7,6 +7,7 @@ import { z } from "zod";
import { logger } from "."; import { logger } from ".";
import { getRequest } from "@tanstack/react-start/server"; import { getRequest } from "@tanstack/react-start/server";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const fetchMe = createServerFn() export const fetchMe = createServerFn()
.handler(async () => .handler(async () =>
@@ -46,7 +47,7 @@ export const getPlayer = createServerFn()
export const updatePlayer = createServerFn() export const updatePlayer = createServerFn()
.inputValidator(playerUpdateSchema) .inputValidator(playerUpdateSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
const userAuthId = context.userAuthId; const userAuthId = context.userAuthId;
@@ -98,7 +99,7 @@ export const createPlayer = createServerFn()
export const associatePlayer = createServerFn() export const associatePlayer = createServerFn()
.inputValidator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
const userAuthId = context.userAuthId; const userAuthId = context.userAuthId;

View File

@@ -6,6 +6,7 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { teamInputSchema, teamUpdateSchema } from "./types"; import { teamInputSchema, teamUpdateSchema } from "./types";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import { Match } from "../matches/types"; import { Match } from "../matches/types";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const listTeamInfos = createServerFn() export const listTeamInfos = createServerFn()
@@ -30,7 +31,7 @@ export const getTeamInfo = createServerFn()
export const createTeam = createServerFn() export const createTeam = createServerFn()
.inputValidator(teamInputSchema) .inputValidator(teamInputSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data, context }) => .handler(async ({ data, context }) =>
toServerResult(async () => { toServerResult(async () => {
const userId = context.userAuthId; const userId = context.userAuthId;
@@ -50,7 +51,7 @@ export const updateTeam = createServerFn()
id: z.string(), id: z.string(),
updates: teamUpdateSchema updates: teamUpdateSchema
})) }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { id, updates }, context }) => .handler(async ({ data: { id, updates }, context }) =>
toServerResult(async () => { toServerResult(async () => {
const userId = context.userAuthId; const userId = context.userAuthId;
@@ -61,10 +62,10 @@ export const updateTeam = createServerFn()
throw new Error("Team not found"); throw new Error("Team not found");
} }
const isPlayerOnTeam = team.players.some(player => player.id === userId); //const isPlayerOnTeam = team.players.some(player => player.id === userId);
if (!isAdmin && !isPlayerOnTeam) { //if (!isAdmin && !isPlayerOnTeam) {
throw new Error("You can only update teams that you are a member of"); // throw new Error("You can only update teams that you are a member of");
} // }
logger.info("Updating team", { teamId: id, userId, isAdmin }); logger.info("Updating team", { teamId: id, userId, isAdmin });
return pbAdmin.updateTeam(id, updates); return pbAdmin.updateTeam(id, updates);

View File

@@ -5,6 +5,7 @@ import { tournamentInputSchema } from "@/features/tournaments/types";
import { logger } from "."; import { logger } from ".";
import { z } from "zod"; import { z } from "zod";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import { serverFnLoggingMiddleware } from "@/utils/activities";
export const listTournaments = createServerFn() export const listTournaments = createServerFn()
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
@@ -14,7 +15,7 @@ export const listTournaments = createServerFn()
export const createTournament = createServerFn() export const createTournament = createServerFn()
.inputValidator(tournamentInputSchema) .inputValidator(tournamentInputSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult(() => pbAdmin.createTournament(data)) toServerResult(() => pbAdmin.createTournament(data))
); );
@@ -24,7 +25,7 @@ export const updateTournament = createServerFn()
id: z.string(), id: z.string(),
updates: tournamentInputSchema.partial() updates: tournamentInputSchema.partial()
})) }))
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult(() => pbAdmin.updateTournament(data.id, data.updates)) toServerResult(() => pbAdmin.updateTournament(data.id, data.updates))
); );
@@ -48,7 +49,7 @@ export const enrollTeam = createServerFn()
tournamentId: z.string(), tournamentId: z.string(),
teamId: z.string() teamId: z.string()
})) }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, teamId }, context }) => .handler(async ({ data: { tournamentId, teamId }, context }) =>
toServerResult(async () => { toServerResult(async () => {
const userId = context.userAuthId; const userId = context.userAuthId;
@@ -57,11 +58,11 @@ export const enrollTeam = createServerFn()
const team = await pbAdmin.getTeam(teamId); const team = await pbAdmin.getTeam(teamId);
if (!team) { throw new Error('Team not found'); } if (!team) { throw new Error('Team not found'); }
const isPlayerOnTeam = team.players?.some(player => player.id === userId); //const isPlayerOnTeam = team.players?.some(player => player.id === userId);
if (!isPlayerOnTeam && !isAdmin) { //if (!isPlayerOnTeam && !isAdmin) {
throw new Error('You do not have permission to enroll this team'); // throw new Error('You do not have permission to enroll this team');
} //}
logger.info('Enrolling team in tournament', { tournamentId, teamId, userId }); logger.info('Enrolling team in tournament', { tournamentId, teamId, userId });
const tournament = await pbAdmin.enrollTeam(tournamentId, teamId); const tournament = await pbAdmin.enrollTeam(tournamentId, teamId);
@@ -74,7 +75,7 @@ export const unenrollTeam = createServerFn()
tournamentId: z.string(), tournamentId: z.string(),
teamId: z.string() teamId: z.string()
})) }))
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId, teamId }, context }) => .handler(async ({ data: { tournamentId, teamId }, context }) =>
toServerResult(() => pbAdmin.unenrollTeam(tournamentId, teamId)) toServerResult(() => pbAdmin.unenrollTeam(tournamentId, teamId))
); );
@@ -95,7 +96,7 @@ export const getFreeAgents = createServerFn()
export const enrollFreeAgent = createServerFn() export const enrollFreeAgent = createServerFn()
.inputValidator(z.object({ phone: z.string(), tournamentId: z.string() })) .inputValidator(z.object({ phone: z.string(), tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
const userAuthId = context.userAuthId; const userAuthId = context.userAuthId;
@@ -109,7 +110,7 @@ export const enrollFreeAgent = createServerFn()
export const unenrollFreeAgent = createServerFn() export const unenrollFreeAgent = createServerFn()
.inputValidator(z.object({ tournamentId: z.string() })) .inputValidator(z.object({ tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
const userAuthId = context.userAuthId; const userAuthId = context.userAuthId;

View File

@@ -19,6 +19,7 @@ const eventHandlers: Record<string, EventHandler> = {
logger.info("New Connection"); logger.info("New Connection");
}, },
"ping": () => {}, "ping": () => {},
"heartbeat": () => {},
"match": (event, queryClient) => { "match": (event, queryClient) => {
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId)) queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
queryClient.invalidateQueries(tournamentQueries.current()) queryClient.invalidateQueries(tournamentQueries.current())
@@ -73,15 +74,15 @@ export function useServerEvents() {
logger.error("SSE connection error", error); logger.error("SSE connection error", error);
eventSource.close(); eventSource.close();
if (shouldConnectRef.current && retryCountRef.current < 5) { if (shouldConnectRef.current && retryCountRef.current < 10) {
retryCountRef.current += 1; retryCountRef.current += 1;
const delay = Math.min( const delay = Math.min(
1000 * Math.pow(2, retryCountRef.current - 1), 1000 * Math.pow(1.5, retryCountRef.current - 1),
30000 15000
); );
logger.info( logger.info(
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms` `SSE reconnection attempt ${retryCountRef.current}/10 in ${delay}ms`
); );
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
@@ -89,7 +90,7 @@ export function useServerEvents() {
connectEventSource(); connectEventSource();
} }
}, delay); }, delay);
} else if (retryCountRef.current >= 5) { } else if (retryCountRef.current >= 10) {
logger.error("SSE max reconnection attempts reached"); logger.error("SSE max reconnection attempts reached");
} }
}; };

View File

@@ -2,6 +2,23 @@ import { EventEmitter } from "events";
export const serverEvents = new EventEmitter(); export const serverEvents = new EventEmitter();
serverEvents.setMaxListeners(50);
// Debug logging for listener count
if (process.env.NODE_ENV === 'development') {
setInterval(() => {
const listenerCounts = {
test: serverEvents.listenerCount('test'),
match: serverEvents.listenerCount('match'),
reaction: serverEvents.listenerCount('reaction'),
};
if (listenerCounts.test > 0 || listenerCounts.match > 0 || listenerCounts.reaction > 0) {
console.log('ServerEvents listener count:', listenerCounts);
}
}, 30000); // Log every 30 seconds in development
}
export type TestEvent = { export type TestEvent = {
type: "test"; type: "test";
playerId: string; playerId: string;

View File

@@ -4,6 +4,7 @@ import { createTournamentsService } from "./services/tournaments";
import { createTeamsService } from "./services/teams"; import { createTeamsService } from "./services/teams";
import { createMatchesService } from "./services/matches"; import { createMatchesService } from "./services/matches";
import { createReactionsService } from "./services/reactions"; import { createReactionsService } from "./services/reactions";
import { createActivitiesService } from "./services/activities";
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
@@ -35,6 +36,7 @@ class PocketBaseAdminClient {
Object.assign(this, createTournamentsService(this.pb)); Object.assign(this, createTournamentsService(this.pb));
Object.assign(this, createMatchesService(this.pb)); Object.assign(this, createMatchesService(this.pb));
Object.assign(this, createReactionsService(this.pb)); Object.assign(this, createReactionsService(this.pb));
Object.assign(this, createActivitiesService(this.pb));
}); });
} }
@@ -54,7 +56,8 @@ interface AdminClient
ReturnType<typeof createTeamsService>, ReturnType<typeof createTeamsService>,
ReturnType<typeof createTournamentsService>, ReturnType<typeof createTournamentsService>,
ReturnType<typeof createMatchesService>, ReturnType<typeof createMatchesService>,
ReturnType<typeof createReactionsService> { ReturnType<typeof createReactionsService>,
ReturnType<typeof createActivitiesService> {
authPromise: Promise<void>; authPromise: Promise<void>;
} }

View File

@@ -0,0 +1,56 @@
import PocketBase from "pocketbase";
export interface Activity {
id: string;
name: string;
player?: string;
duration: number;
success: boolean;
error?: string;
arguments?: any;
user_agent?: string;
created: string;
updated: string;
}
export interface ActivityInput {
name: string;
player?: string;
duration: number;
success: boolean;
error?: string;
arguments?: any;
user_agent?: string;
}
export function createActivitiesService(pb: PocketBase) {
return {
async createActivity(data: ActivityInput): Promise<Activity> {
const result = await pb.collection("activities").create<Activity>(data);
return result;
},
async getRecentActivities(limit: number = 100): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
sort: "-created",
});
return result.items;
},
async getActivitiesByUser(userId: string, limit: number = 50): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
return result.items;
},
async getActivitiesByFunction(functionName: string, limit: number = 50): Promise<Activity[]> {
const result = await pb.collection("activities").getList<Activity>(1, limit, {
filter: `function_name = "${functionName}"`,
sort: "-created",
});
return result.items;
},
};
}

View File

@@ -6,7 +6,7 @@ import UserRoles from "supertokens-node/recipe/userroles";
import { appInfo } from "./config"; import { appInfo } from "./config";
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode"; import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
import { logger } from "./"; import { logger } from "./";
import passwordlessTwilioVerify from "./recipes/passwordless-twilio-verify"; import PasswordlessTwilioVerify from "./recipes/passwordless-twilio-verify";
export const backendConfig = (): TypeInput => { export const backendConfig = (): TypeInput => {
return { return {
@@ -17,7 +17,8 @@ export const backendConfig = (): TypeInput => {
}, },
appInfo, appInfo,
recipeList: [ recipeList: [
passwordlessTwilioVerify.init(), //PasswordlessTwilioVerify.init(),
PasswordlessDevelopmentMode.init(),
Session.init({ Session.init({
cookieSameSite: "lax", cookieSameSite: "lax",
cookieSecure: import.meta.env.NODE_ENV === "production", cookieSecure: import.meta.env.NODE_ENV === "production",

View File

@@ -1,19 +1,31 @@
import twilio, { type Twilio } from "twilio"; import twilio, { type Twilio } from "twilio";
const accountSid = process.env.TWILIO_ACCOUNT_SID!;
const authToken = process.env.TWILIO_AUTH_TOKEN!;
const serviceSid = process.env.TWILIO_SERVICE_SID!;
let client: Twilio; let client: Twilio;
function getEnvVars() {
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const serviceSid = process.env.TWILIO_SERVICE_SID;
if (!accountSid || !authToken || !serviceSid) {
throw new Error(`Missing env vars. accountSid: ${!!accountSid}, authToken: ${!!authToken}, serviceSid: ${!!serviceSid}`);
}
return { accountSid, authToken, serviceSid };
}
function getTwilioClient() { function getTwilioClient() {
if (!client) { if (!client) {
const { accountSid, authToken } = getEnvVars();
client = twilio(accountSid, authToken); client = twilio(accountSid, authToken);
} }
return client; return client;
} }
export async function sendVerifyCode(phoneNumber: string, code: string) { export async function sendVerifyCode(phoneNumber: string, code: string) {
const { serviceSid } = getEnvVars();
const twilioClient = getTwilioClient(); const twilioClient = getTwilioClient();
const verification = await twilioClient!.verify.v2 const verification = await twilioClient!.verify.v2
@@ -32,6 +44,7 @@ export async function sendVerifyCode(phoneNumber: string, code: string) {
} }
export async function updateVerify(sid: string) { export async function updateVerify(sid: string) {
const { serviceSid } = getEnvVars();
const twilioClient = getTwilioClient(); const twilioClient = getTwilioClient();
const verification = await twilioClient!.verify.v2 const verification = await twilioClient!.verify.v2

54
src/utils/activities.ts Normal file
View File

@@ -0,0 +1,54 @@
import { pbAdmin } from "@/lib/pocketbase/client";
import { createMiddleware } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
export const serverFnLoggingMiddleware = createMiddleware({
type: "function",
}).server(async ({ next, data, functionId, context }) => {
const request = getRequest();
const serverFnName = functionId.split('--')[1]?.split('_')[0] || 'unknown';
const userId = (context as any)?.metadata?.player_id || 'unknown';
const startTime = Date.now();
try {
const result = await next();
const duration = Date.now() - startTime;
try {
await pbAdmin.authPromise;
await pbAdmin.createActivity({
name: serverFnName,
player: userId !== 'unknown' ? userId : undefined,
duration,
success: true,
arguments: data,
user_agent: request.headers.get('user-agent') || undefined,
});
} catch (activityError) {
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
try {
await pbAdmin.authPromise;
await pbAdmin.createActivity({
name: serverFnName,
player: userId !== 'unknown' ? userId : undefined,
duration,
success: false,
error: errorMessage,
arguments: data,
user_agent: request.headers.get('user-agent') || undefined,
});
} catch (activityError) {
}
throw error;
}
});