Compare commits
10 Commits
upgrade
...
63ea515a31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63ea515a31 | ||
|
|
8b1bbe213d | ||
|
|
ed538b7373 | ||
|
|
03e3bbcbc0 | ||
|
|
baf75eddba | ||
|
|
5094933302 | ||
|
|
9564b46d45 | ||
|
|
ece5094f13 | ||
|
|
cfe1ee7171 | ||
|
|
3a41609a91 |
108
pb_migrations/1759244692_created_activities.js
Normal file
108
pb_migrations/1759244692_created_activities.js
Normal 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);
|
||||||
|
})
|
||||||
27
pb_migrations/1759245857_updated_activities.js
Normal file
27
pb_migrations/1759245857_updated_activities.js
Normal 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)
|
||||||
|
})
|
||||||
43
pb_migrations/1759246171_updated_activities.js
Normal file
43
pb_migrations/1759246171_updated_activities.js
Normal 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)
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
src/lib/pocketbase/services/activities.ts
Normal file
56
src/lib/pocketbase/services/activities.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
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
|
||||||
.services(serviceSid)
|
.services(serviceSid)
|
||||||
.verifications.create({
|
.verifications.create({
|
||||||
@@ -32,8 +44,9 @@ 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
|
||||||
.services(serviceSid)
|
.services(serviceSid)
|
||||||
.verifications(sid)
|
.verifications(sid)
|
||||||
|
|||||||
54
src/utils/activities.ts
Normal file
54
src/utils/activities.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user