[admin] feat: complete critical logs table

This commit is contained in:
Yangshun 2025-07-09 10:19:49 +08:00
parent 611dbc37e9
commit 52d4e8a2db
10 changed files with 625 additions and 191 deletions

View File

@ -1,20 +1,3 @@
REDDIT_CLIENT_ID=your_client_id
REDDIT_CLIENT_SECRET=your_client_secret
REDDIT_USER_AGENT=your_user_agent
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[SUPABASE_REFERENCE_ID].supabase.co:6543/postgres?pgbouncer=true&connection_limit=30&pool_timeout=600
DIRECT_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[SUPABASE_REFERENCE_ID].supabase.com:5432/postgres
NEXTAUTH_SECRET=secret_key
NEXTAUTH_URL=http://localhost:3000
AUTH_GOOGLE_ID=google_id
AUTH_GOOGLE_SECRET=google_secret
PASSWORD_KEY=some_random_value
GOOGLE_CHAT_WEBHOOK_URL=google_chat_webhook_url
CRON_SECRET=some_random_value
AXIOM_ORG_ID=
AXIOM_TOKEN=
DATABASE_URL=

View File

@ -1,22 +1,10 @@
// @ts-check
import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin';
/**
* @type {import('next').NextConfig}
**/
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
reactStrictMode: true,
experimental: {
optimizePackageImports: ['@mantine/core', '@mantine/hooks'],
},
webpack: (config, { isServer }) => {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()];
}
return config;
},
};
export default nextConfig;

View File

@ -12,8 +12,12 @@
"tsc": "tsc"
},
"dependencies": {
"@axiomhq/js": "1.3.1",
"clsx": "^2.1.1",
"date-fns": "2.30.0",
"lodash-es": "^4.17.21",
"next": "14.2.15",
"pg": "^8.16.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@ -25,6 +29,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/pg": "^8.15.4",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"eslint": "^9.26.0",

View File

@ -0,0 +1,95 @@
import { Axiom } from '@axiomhq/js';
import { startOfDay, subDays } from 'date-fns';
import { Client as PgClient } from 'pg';
const daysBefore = 30;
const pgClient = new PgClient({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
});
const aplQuery = `
events
| extend shifted_time = _time + 8h
| summarize
CheckoutSuccess = countif(['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews'),
CheckoutInitiate = dcountif(['user.fingerprint'], ['event.name'] == 'checkout.attempt' and ['event.payload.namespace'] == 'interviews'),
NumVisits = dcountif(
['user.fingerprint'],
(['event.name'] == 'pageview'
and ['request.pathname'] !startswith '/projects'
)),
NumFirstVisits = dcountif(
['user.fingerprint'],
(['event.name'] == 'pageview'
and ['request.pathname'] !startswith '/projects'
and todatetime(['user.firstVisit']) > (_time - 24h)
)),
CheckoutSuccessSameDay = dcountif(['user.email'], ['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews' and todatetime(['user.firstVisit']) > (_time - 24h)),
CheckoutInitiateSameDay = dcountif(['user.email'], ['event.name'] == 'checkout.attempt' and ['event.payload.namespace'] == 'interviews' and todatetime(['user.firstVisit']) > (_time - 24h)),
SignUps = dcountif(
['user.fingerprint'],
(['event.name'] == 'auth.sign_up'
))
by ['time'] = startofday(shifted_time)
| extend CheckoutInitiateRate = round(100.0 * CheckoutInitiate / NumVisits, 2)
| extend CheckoutSuccessRate = round(100.0 * CheckoutSuccess / NumVisits, 2)
| extend CheckoutInitiateSameDayRate = round(100.0 * CheckoutInitiateSameDay / NumFirstVisits, 2)
| extend CheckoutSuccessSameDayRate = round(100.0 * CheckoutSuccessSameDay / NumFirstVisits, 2)
| order by _time desc
`;
const pgQuerySignUps = `SELECT
date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date,
count(*) AS "signUps"
FROM
auth.users
WHERE
created_at >= NOW() - INTERVAL '${daysBefore} days'
GROUP BY
date
ORDER BY date DESC;`;
const pgQueryEmailSignUps = `SELECT
date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date,
count(*) AS "emailSignUps",
count(email_confirmed_at) AS "confirmedEmailSignUps"
FROM
auth.users
WHERE
confirmation_sent_at IS NOT NULL AND
created_at >= NOW() - INTERVAL '${daysBefore} days'
GROUP BY
date
ORDER BY
date DESC;`;
export async function GET(_request: Request) {
const axiom = new Axiom({
orgId: process.env.AXIOM_ORG_ID!,
token: process.env.AXIOM_TOKEN!,
});
try {
await pgClient.connect();
} catch (error) {
// Ignore
}
const [axiomRes, pgResSignUps, pgResEmailSignUps] = await Promise.all([
axiom.query(aplQuery, {
startTime: startOfDay(subDays(new Date(), daysBefore)).toISOString(),
}),
pgClient.query(pgQuerySignUps),
pgClient.query(pgQueryEmailSignUps),
]);
pgClient.end();
return Response.json({
conversions: axiomRes.matches,
emailSignUps: pgResEmailSignUps.rows,
signUps: pgResSignUps.rows,
});
}

View File

@ -3,8 +3,6 @@ import type { Metadata } from 'next';
import '~/styles/globals.css';
import RootLayout from '../components/RootLayout';
type Props = Readonly<{
children: React.ReactNode;
}>;
@ -17,9 +15,7 @@ export const metadata: Metadata = {
export default function Layout({ children }: Props) {
return (
<html lang="en">
<body className={clsx('antialiased', 'bg-white')}>
<RootLayout>{children}</RootLayout>
</body>
<body className={clsx('antialiased', 'bg-white')}>{children}</body>
</html>
);
}

View File

@ -1,7 +1,5 @@
import ConversionsPage from '~/components/ConversionsPage';
export default function Page() {
return (
<div className="flex h-screen items-center justify-center">
<h1 className="text-2xl font-bold">Admin Dashboard</h1>
</div>
);
return <ConversionsPage />;
}

View File

@ -0,0 +1,40 @@
'use client';
import { useEffect, useState } from 'react';
import type { DayData, EmailSignupData, SignupData } from './ConversionsTable';
import ConversionsTable from './ConversionsTable';
export default function ConversionsPage() {
const [data, setData] = useState<{
conversions: ReadonlyArray<DayData>;
emailSignUps: ReadonlyArray<EmailSignupData>;
signUps: ReadonlyArray<SignupData>;
} | null>(null);
useEffect(() => {
// This is where you would fetch conversion data or any other necessary data
// For example, you could use fetch or axios to get data from an API endpoint
fetch('/api/conversions')
.then((response) => response.json())
.then((data_) => {
setData(data_);
})
.catch((error) => {
console.error('Error fetching conversion data:', error);
});
}, []);
return (
<div className="flex flex-col gap-2 p-4">
<h1 className="text-2xl font-bold">Critical data</h1>
{data && (
<ConversionsTable
conversions={data.conversions}
emailSignUps={data.emailSignUps}
signUps={data.signUps}
/>
)}
</div>
);
}

View File

@ -0,0 +1,305 @@
import clsx from 'clsx';
import { addDays } from 'date-fns';
/* eslint-disable react/no-array-index-key */
const thClassname =
'sticky top-0 z-10 px-2 py-2 text-left text-sm font-semibold text-gray-100 bg-gray-900 align-top';
const tdClassname = 'px-2 py-2 text-sm text-gray-900';
export type SignupData = Readonly<{
date: string;
signUps: string;
}>;
export type EmailSignupData = Readonly<{
confirmedEmailSignUps: string;
date: string;
emailSignUps: string;
}>;
export type DayData = Readonly<{
_rowId: string;
_time: string;
data: Readonly<{
CheckoutInitiate: number;
CheckoutInitiateRate: string;
CheckoutInitiateSameDay: number;
CheckoutInitiateSameDayRate: string;
CheckoutSuccess: number;
CheckoutSuccessRate: string;
CheckoutSuccessSameDay: number;
CheckoutSuccessSameDayRate: string;
NumFirstVisits: number;
NumVisits: number;
time: string;
}>;
}>;
const startOfWeekDay = 1; // Monday
const dateFormat = {
day: '2-digit',
month: 'short',
} as const;
// A helper to parse percent values safely
function parsePercent(value: string) {
const num = parseFloat(value);
return isNaN(num) ? 0 : num;
}
export default function ConversionsTable({
conversions,
emailSignUps,
signUps,
}: {
conversions: ReadonlyArray<DayData>;
emailSignUps: ReadonlyArray<EmailSignupData>;
signUps: ReadonlyArray<SignupData>;
}) {
const conversionsRows = conversions
.slice()
.sort(
(a, b) =>
new Date(a.data.time).getTime() - new Date(b.data.time).getTime(),
);
const signUpByDate = signUps.reduce((acc, signup, index) => {
const date = new Date(signup.date).toLocaleDateString('en-SG');
const emailSignUpDay = emailSignUps[index];
return {
...acc,
[date]: {
confirmedEmailSignUps: Number(emailSignUpDay.confirmedEmailSignUps),
emailSignUps: Number(emailSignUpDay.emailSignUps),
signUps: Number(signup.signUps),
},
};
}, {}) as Record<
string,
{ confirmedEmailSignUps: number; emailSignUps: number; signUps: number }
>;
const firstStartOfWeekIndex = conversionsRows.findIndex((row) => {
const date = new Date(row.data.time);
return date.getDay() === startOfWeekDay;
});
// Precompute numeric values per column for percentile calculation
const numericColumnValues: Record<string, Array<number>> = {
CheckoutInitiate: conversionsRows.map((r) => r.data.CheckoutInitiate),
CheckoutInitiateRate: conversionsRows.map((r) =>
parsePercent(r.data.CheckoutInitiateRate),
),
CheckoutInitiateSameDayRate: conversionsRows.map((r) =>
parsePercent(r.data.CheckoutInitiateSameDayRate),
),
CheckoutSuccess: conversionsRows.map((r) => r.data.CheckoutSuccess),
CheckoutSuccessRate: conversionsRows.map((r) =>
parsePercent(r.data.CheckoutSuccessRate),
),
CheckoutSuccessSameDay: conversionsRows.map(
(r) => r.data.CheckoutSuccessSameDay,
),
CheckoutSuccessSameDayRate: conversionsRows.map((r) =>
parsePercent(r.data.CheckoutSuccessSameDayRate),
),
NumFirstVisits: conversionsRows.map((r) => r.data.NumFirstVisits),
NumVisits: conversionsRows.map((r) => r.data.NumVisits),
};
const columns: Array<{
getValue: (
row: DayData,
signUp:
| {
confirmedEmailSignUps: number;
emailSignUps: number;
signUps: number;
}
| undefined,
) => React.ReactNode;
header: React.ReactNode;
key: string | null; // Null for non-numeric columns
}> = [
{
getValue: (row) =>
new Date(row.data.time).toLocaleDateString('en-US', dateFormat),
header: 'Date',
key: null,
},
{
getValue: (row) => row.data.NumVisits,
header: 'Non-purchasers',
key: 'NumVisits',
},
{
getValue: (row) => row.data.NumFirstVisits,
header: 'First visits',
key: 'NumFirstVisits',
},
{
getValue: (row) => row.data.CheckoutInitiate,
header: 'Checkout initiates',
key: 'CheckoutInitiate',
},
{
getValue: (row) => `${row.data.CheckoutInitiateRate}%`,
header: <>Checkout initiate rate</>,
key: 'CheckoutInitiateRate',
},
{
getValue: (row) => `${row.data.CheckoutInitiateSameDayRate}%`,
header: (
<>
Checkout initiate rate
<br />
(same day)
</>
),
key: 'CheckoutInitiateSameDayRate',
},
{
getValue: (row) => row.data.CheckoutSuccess,
header: 'New payments',
key: 'CheckoutSuccess',
},
{
getValue: (row) => `${row.data.CheckoutSuccessRate}%`,
header: 'Conversion rate',
key: 'CheckoutSuccessRate',
},
{
getValue: (row) => row.data.CheckoutSuccessSameDay,
header: (
<>
New payments
<br />
(same day)
</>
),
key: 'CheckoutSuccessSameDay',
},
{
getValue: (row) => `${row.data.CheckoutSuccessSameDayRate}%`,
header: (
<>
Conversion rate
<br />
(same day)
</>
),
key: 'CheckoutSuccessSameDayRate',
},
{
getValue: (_row, signUp) => signUp?.signUps ?? 0,
header: 'Signups',
key: null,
},
{
getValue: (_row, signUp) =>
signUp && signUp.signUps > 0
? `${((signUp.confirmedEmailSignUps / signUp.emailSignUps) * 100).toFixed(2)}%`
: '-',
header: 'Email verification rate',
key: null,
},
];
// Helper to compute percentile rank
function getPercentile(value: number, allValues: Array<number>): number {
const sorted = [...allValues].sort((a, b) => a - b);
const index = sorted.findIndex((v) => v >= value);
const rank = index === -1 ? sorted.length : index + 1;
return (rank / sorted.length) * 100;
}
// Helper to get background color
function getBackground(percentile: number): string {
const bucket = Math.floor(percentile / 10) * 10;
if (bucket <= 5) return 'bg-red-500/50';
if (bucket <= 15) return 'bg-red-400/50';
if (bucket <= 25) return 'bg-red-300/50';
if (bucket <= 35) return 'bg-red-200/50';
if (bucket <= 45) return 'bg-red-100/50';
if (bucket <= 55) return 'bg-white';
if (bucket <= 65) return 'bg-green-100/50';
if (bucket <= 75) return 'bg-green-200/50';
if (bucket <= 85) return 'bg-green-300/50';
if (bucket <= 95) return 'bg-green-400/50';
return 'bg-green-500/50';
}
return (
<table className="min-w-full border border-gray-200">
<thead>
<tr className="bg-gray-800">
<th className={thClassname} scope="col">
Week
</th>
{columns.map((col, i) => (
<th key={i} className={thClassname} scope="col">
{col.header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{conversionsRows.map((row, index) => {
const date = new Date(row.data.time);
const isStartOfWeek = date.getDay() === startOfWeekDay;
const signUp = signUpByDate[date.toLocaleDateString('en-SG')];
return (
<tr key={row._rowId}>
{index === 0 && !isStartOfWeek && (
<td
className={clsx(tdClassname, 'border-r border-gray-200')}
rowSpan={firstStartOfWeekIndex}
/>
)}
{isStartOfWeek && (
<td
className={clsx(tdClassname, 'border-r border-gray-200')}
rowSpan={7}>
{date.toLocaleDateString('en-US', dateFormat)} to{' '}
{addDays(date, 7).toLocaleDateString('en-US', dateFormat)}
</td>
)}
{columns.map((col, index_) => {
let bgClass = '';
if (col.key && numericColumnValues[col.key]) {
const rawValue = col.key.endsWith('Rate')
? // @ts-expect-error: vibe coded
parsePercent(row.data[col.key])
: // @ts-expect-error: vibe coded
row.data[col.key];
const percentile = getPercentile(
rawValue,
numericColumnValues[col.key],
);
bgClass = getBackground(percentile);
}
return (
<td key={index_} className={clsx(tdClassname, bgClass)}>
{col.getValue(row, signUp)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
}

View File

@ -1,9 +0,0 @@
'use client';
type Props = Readonly<{
children: React.ReactNode;
}>;
export default function RootLayout({ children }: Props) {
return <>{children}</>;
}

View File

@ -20,12 +20,24 @@ importers:
apps/admin:
dependencies:
'@axiomhq/js':
specifier: 1.3.1
version: 1.3.1
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: 2.30.0
version: 2.30.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
next:
specifier: 14.2.15
version: 14.2.15(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
pg:
specifier: ^8.16.3
version: 8.16.3
react:
specifier: ^18.2.0
version: 18.3.1
@ -54,6 +66,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.12.11)(@types/node@22.15.0)(typescript@5.8.3)))
'@types/pg':
specifier: ^8.15.4
version: 8.15.4
'@types/react':
specifier: 18.0.28
version: 18.0.28
@ -271,7 +286,7 @@ importers:
version: 10.0.0
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.3)
version: 10.4.21(postcss@8.5.6)
dotenv-cli:
specifier: ^7.3.0
version: 7.4.4
@ -280,13 +295,13 @@ importers:
version: 9.26.0(jiti@1.21.7)
postcss:
specifier: ^8.4.32
version: 8.5.3
version: 8.5.6
postcss-preset-mantine:
specifier: ^1.11.1
version: 1.17.0(postcss@8.5.3)
version: 1.17.0(postcss@8.5.6)
postcss-simple-vars:
specifier: ^7.0.1
version: 7.0.1(postcss@8.5.3)
version: 7.0.1(postcss@8.5.6)
prettier:
specifier: 3.5.3
version: 3.5.3
@ -725,13 +740,13 @@ importers:
version: 1.18.8
'@vitejs/plugin-react':
specifier: ^4.4.1
version: 4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))
version: 4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))
'@vitest/ui':
specifier: ^3.1.2
version: 3.1.3(vitest@3.1.3)
autoprefixer:
specifier: ^10.4.13
version: 10.4.21(postcss@8.5.3)
version: 10.4.21(postcss@8.5.6)
chalk:
specifier: ^5.2.0
version: 5.4.1
@ -776,7 +791,7 @@ importers:
version: 3.1.55(@next/env@14.2.15)(next@14.2.15(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
postcss:
specifier: ^8.4.21
version: 8.5.3
version: 8.5.6
prettier-plugin-tailwindcss:
specifier: ^0.5.9
version: 0.5.14(prettier-plugin-svelte@3.2.5(prettier@3.3.2)(svelte@5.30.1))(prettier@3.3.2)
@ -806,10 +821,10 @@ importers:
version: 5.8.3
vite-tsconfig-paths:
specifier: ^5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))
vitest:
specifier: ^3.0.2
version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
packages/eslint-config:
devDependencies:
@ -1201,6 +1216,10 @@ packages:
resolution: {integrity: sha512-4xf0b/KFVFmNvjxK4lHHuwioQ6TPr+PZLqEdMl/vcot1RqFwDnjaOEOp0/VPhyTgdE0di8bl/hOsu33bHMM27w==}
engines: {node: '>=16'}
'@axiomhq/js@1.3.1':
resolution: {integrity: sha512-Ytf5V3wKz8FKNiqJxnqZmUhjgJ7TItKUoyHVNE/H2V9dN1ozD6NNnsueenOjKdA48cm2sGRyP432nworst18aA==}
engines: {node: '>=16'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@ -4474,6 +4493,9 @@ packages:
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
'@types/pg@8.15.4':
resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==}
'@types/phoenix@1.6.6':
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
@ -8240,6 +8262,40 @@ packages:
periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
pg-cloudflare@1.2.7:
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
pg-connection-string@2.9.1:
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.10.1:
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.16.3:
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -8554,14 +8610,26 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
@ -9446,6 +9514,10 @@ packages:
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@ -10226,6 +10298,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
@ -10528,6 +10604,10 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -11145,6 +11225,11 @@ snapshots:
fetch-retry: 6.0.0
uuid: 8.3.2
'@axiomhq/js@1.3.1':
dependencies:
fetch-retry: 6.0.0
uuid: 11.1.0
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@ -14871,6 +14956,12 @@ snapshots:
'@types/parse5@6.0.3': {}
'@types/pg@8.15.4':
dependencies:
'@types/node': 22.15.0
pg-protocol: 1.10.3
pg-types: 2.2.0
'@types/phoenix@1.6.6': {}
'@types/picomatch@3.0.2': {}
@ -15031,7 +15122,7 @@ snapshots:
'@use-gesture/core': 10.3.1
react: 18.3.1
'@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))':
'@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
@ -15039,7 +15130,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.11
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@ -15050,14 +15141,6 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 3.1.3
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 3.1.3
@ -15094,7 +15177,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.13
tinyrainbow: 2.0.0
vitest: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
vitest: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/utils@3.1.3':
dependencies:
@ -15410,14 +15493,14 @@ snapshots:
attr-accept@2.2.5: {}
autoprefixer@10.4.21(postcss@8.5.3):
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.24.5
caniuse-lite: 1.0.30001718
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.5.3
postcss: 8.5.6
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
@ -19843,6 +19926,41 @@ snapshots:
estree-walker: 3.0.3
is-reference: 3.0.3
pg-cloudflare@1.2.7:
optional: true
pg-connection-string@2.9.1: {}
pg-int8@1.0.1: {}
pg-pool@3.10.1(pg@8.16.3):
dependencies:
pg: 8.16.3
pg-protocol@1.10.3: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.16.3:
dependencies:
pg-connection-string: 2.9.1
pg-pool: 3.10.1(pg@8.16.3)
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.2.7
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@ -19912,11 +20030,6 @@ snapshots:
read-cache: 1.0.0
resolve: 1.22.10
postcss-js@4.0.1(postcss@8.5.3):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.3
postcss-js@4.0.1(postcss@8.5.6):
dependencies:
camelcase-css: 2.0.1
@ -19978,13 +20091,13 @@ snapshots:
postcss: 8.5.6
postcss-selector-parser: 7.1.0
postcss-mixins@9.0.4(postcss@8.5.3):
postcss-mixins@9.0.4(postcss@8.5.6):
dependencies:
fast-glob: 3.3.3
postcss: 8.5.3
postcss-js: 4.0.1(postcss@8.5.3)
postcss-simple-vars: 7.0.1(postcss@8.5.3)
sugarss: 4.0.1(postcss@8.5.3)
postcss: 8.5.6
postcss-js: 4.0.1(postcss@8.5.6)
postcss-simple-vars: 7.0.1(postcss@8.5.6)
sugarss: 4.0.1(postcss@8.5.6)
postcss-modules-extract-imports@3.1.0(postcss@8.5.6):
dependencies:
@ -20019,11 +20132,6 @@ snapshots:
postcss-modules-values: 4.0.0(postcss@8.5.6)
string-hash: 1.1.3
postcss-nested@6.2.0(postcss@8.5.3):
dependencies:
postcss: 8.5.3
postcss-selector-parser: 6.1.2
postcss-nested@6.2.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
@ -20080,11 +20188,11 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-preset-mantine@1.17.0(postcss@8.5.3):
postcss-preset-mantine@1.17.0(postcss@8.5.6):
dependencies:
postcss: 8.5.3
postcss-mixins: 9.0.4(postcss@8.5.3)
postcss-nested: 6.2.0(postcss@8.5.3)
postcss: 8.5.6
postcss-mixins: 9.0.4(postcss@8.5.6)
postcss-nested: 6.2.0(postcss@8.5.6)
postcss-reduce-initial@7.0.3(postcss@8.5.6):
dependencies:
@ -20112,9 +20220,9 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-simple-vars@7.0.1(postcss@8.5.3):
postcss-simple-vars@7.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.3
postcss: 8.5.6
postcss-svgo@7.0.2(postcss@8.5.6):
dependencies:
@ -20135,18 +20243,22 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.0: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
preact-render-to-string@5.2.6(preact@10.26.6):
dependencies:
preact: 10.26.6
@ -21138,6 +21250,8 @@ snapshots:
spawn-command@0.0.2: {}
split2@4.2.0: {}
sprintf-js@1.0.3: {}
sprintf-js@1.1.3: {}
@ -21341,14 +21455,9 @@ snapshots:
pirates: 4.0.7
ts-interface-checker: 0.1.13
sugarss@4.0.1(postcss@8.5.3):
dependencies:
postcss: 8.5.3
sugarss@4.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
optional: true
supabase@1.226.4:
dependencies:
@ -22019,6 +22128,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@11.1.0: {}
uuid@3.4.0: {}
uuid@8.3.2: {}
@ -22076,27 +22187,6 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite-node@3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-node@3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
@ -22118,34 +22208,17 @@ snapshots:
- tsx
- yaml
vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)):
vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
debug: 4.4.1
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.8.3)
optionalDependencies:
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
postcss: 8.5.6
rollup: 4.40.2
tinyglobby: 0.2.13
optionalDependencies:
'@types/node': 22.15.0
fsevents: 2.3.3
jiti: 1.21.7
sugarss: 4.0.1(postcss@8.5.3)
terser: 5.39.2
tsx: 4.19.4
yaml: 2.8.0
vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.4
@ -22163,48 +22236,6 @@ snapshots:
tsx: 4.19.4
yaml: 2.8.0
vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
'@vitest/expect': 3.1.3
'@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))
'@vitest/pretty-format': 3.1.3
'@vitest/runner': 3.1.3
'@vitest/snapshot': 3.1.3
'@vitest/spy': 3.1.3
'@vitest/utils': 3.1.3
chai: 5.2.0
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.13
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
vite-node: 3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 22.15.0
'@vitest/ui': 3.1.3(vitest@3.1.3)
jsdom: 26.1.0
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.0)(@vitest/ui@3.1.3)(jiti@1.21.7)(jsdom@26.1.0)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
'@vitest/expect': 3.1.3
@ -22447,6 +22478,8 @@ snapshots:
xmlchars@2.2.0: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
yallist@3.1.1: {}