From 25b0414d59fa65e198d315feb2264147cc4d36e2 Mon Sep 17 00:00:00 2001 From: Mantra <87142457+mantrakp04@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:11:06 -0700 Subject: [PATCH] add platform analytics route to the dashboard (#1626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## Summary by cubic Add platform-wide analytics to the internal dashboard with a secure backend route and a new page to visualize cross-project metrics. Only available when viewing the `internal` project and gated by platform admin access. - **New Features** - Backend: add `/api/latest/internal/platform-analytics` aggregating metrics across all projects via ClickHouse; protected by `ensurePlatformAdmin`. - Dashboard: add `/projects/[projectId]/platform-analytics` page with charts; sidebar entry appears only when `projectId === "internal"`. - **Bug Fixes** - Correctness: add `branch_id` filters to all event queries and project aggregates; exclude the `internal` project from ClickHouse aggregates; validate MRR quantity. - Metrics/UI: feature adoption uses `total_projects` from the API and clamps both chart and label to 0–100%; remove unreachable `revenue_growth` sort key. - Safety/Tests: use `Map` for country aggregation; add unit tests for `ensurePlatformAdmin`/`isPlatformAdmin`; switch tests to inline snapshots and document the `as-any` cast. Written for commit 3c803a891536c40bb083c155d2751f9ef2c187f5. Summary will update on new commits. Review in cubic ## Summary by CodeRabbit * **New Features** * Added a Platform Analytics dashboard for internal projects with interactive 7/30-day range charts, KPI tiles, and visual breakdowns (growth, country, sign-in method, user mix), plus email health, dead-click insights, a searchable project leaderboard, and feature adoption. * Introduced an internal analytics API providing rolling-window comparisons and structured metrics for dashboard rendering. * **Bug Fixes** * Strengthened access control with platform-admin authorization for analytics access. * **Tests** * Added coverage for platform-admin authorization behavior. * **Chores** * Updated Next.js to 16.2.9 across applications. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mantra --- apps/backend/package.json | 2 +- .../internal/platform-analytics/route.tsx | 715 ++++++++++++++++++ apps/backend/src/lib/platform-admin.test.ts | 54 ++ apps/backend/src/lib/platform-admin.ts | 25 + apps/dashboard/package.json | 2 +- .../platform-analytics/page-client.tsx | 653 ++++++++++++++++ .../[projectId]/platform-analytics/page.tsx | 9 + .../projects/[projectId]/sidebar-layout.tsx | 19 + apps/internal-tool/package.json | 2 +- apps/mcp/package.json | 2 +- apps/skills/package.json | 2 +- pnpm-lock.yaml | 397 +++++----- 12 files changed, 1660 insertions(+), 222 deletions(-) create mode 100644 apps/backend/src/app/api/latest/internal/platform-analytics/route.tsx create mode 100644 apps/backend/src/lib/platform-admin.test.ts create mode 100644 apps/backend/src/lib/platform-admin.ts create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page-client.tsx create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page.tsx diff --git a/apps/backend/package.json b/apps/backend/package.json index b339b319c..43d272116 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -103,7 +103,7 @@ "jiti": "^2.6.1", "jose": "^6.1.3", "json-diff": "^1.0.6", - "next": "16.2.7", + "next": "16.2.9", "nodemailer": "^6.9.10", "oidc-provider": "^8.5.1", "openid-client": "5.6.4", diff --git a/apps/backend/src/app/api/latest/internal/platform-analytics/route.tsx b/apps/backend/src/app/api/latest/internal/platform-analytics/route.tsx new file mode 100644 index 000000000..83255964e --- /dev/null +++ b/apps/backend/src/app/api/latest/internal/platform-analytics/route.tsx @@ -0,0 +1,715 @@ +import { Prisma } from "@/generated/prisma/client"; +import { getClickhouseAdminClientForMetrics } from "@/lib/clickhouse"; +import { ensurePlatformAdmin } from "@/lib/platform-admin"; +import { DEFAULT_BRANCH_ID } from "@/lib/tenancies"; +import { globalPrismaClient } from "@/prisma-client"; +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { KnownErrors } from "@hexclave/shared"; +import { adaptSchema, clientOrHigherAuthTypeSchema, yupArray, yupNumber, yupObject, yupRecord, yupString } from "@hexclave/shared/dist/schema-fields"; +import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors"; + +// Platform-wide analytics for the internal (platform team) dashboard. Aggregates +// across EVERY customer project in a handful of grouped queries — never N per-project +// calls. Reachable only from the internal project route, which in this deployment is +// the platform team's private dashboard. + +const WINDOW_DAYS = 30; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; +const LEADERBOARD_LIMIT = 500; +const INTERNAL_PROJECT_ID = "internal"; +const AVG_DAYS_PER_MONTH = 365.25 / 12; +const MRR_SUBSCRIPTION_STATUSES = ["active", "trialing"]; +const REVENUE_INVOICE_STATUSES = ["paid", "succeeded"]; + +function ymd(date: Date): string { + return date.toISOString().split("T")[0]; +} + +function chDateTime(date: Date): string { + // ClickHouse DateTime params are "YYYY-MM-DDTHH:MM:SS" with no timezone; treated as UTC. + return date.toISOString().slice(0, 19); +} + +type CountRow = { projectId: string, c: string | number }; + +function rowsToMap(rows: CountRow[]): Map { + const out = new Map(); + for (const row of rows) out.set(row.projectId, Number(row.c)); + return out; +} + +function num(value: unknown): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +// Normalize a single subscription's chosen recurring price to monthly cents. +// Returns 0 for one-time prices (no interval) or missing/non-USD amounts. +function monthlyRecurringCents(product: unknown, priceId: string | null, quantity: number): number { + if (priceId == null || product == null || typeof product !== "object") return 0; + const prices = (product as { prices?: unknown }).prices; + if (prices == null || typeof prices !== "object") return 0; + const price = (prices as Record)[priceId]; + if (price == null || typeof price !== "object") return 0; + const interval = (price as { interval?: unknown }).interval; + if (!Array.isArray(interval) || interval.length < 2) return 0; // one-time purchase + const count = Number(interval[0]); + const unit = String(interval[1]); + const unitMonths = unit === "day" ? 1 / AVG_DAYS_PER_MONTH + : unit === "week" ? 7 / AVG_DAYS_PER_MONTH + : unit === "month" ? 1 + : unit === "year" ? 12 + : 0; + const intervalMonths = count * unitMonths; + if (!(intervalMonths > 0)) return 0; + // Amounts are decimal strings per currency (e.g. "9.99"); we sum USD only. + const usd = (price as Record).USD; + const amount = usd == null ? NaN : Number(usd); + if (!Number.isFinite(amount)) return 0; + if (!Number.isFinite(quantity) || quantity < 0) return 0; + return Math.round((amount * 100 * quantity) / intervalMonths); +} + +const KpiSchema = yupObject({ + value: yupNumber().defined(), + prev: yupNumber().nullable().defined(), +}).defined(); + +const SeriesPointSchema = yupObject({ + date: yupString().defined(), + signups: yupNumber().integer().defined(), + active_users: yupNumber().integer().defined(), + page_views: yupNumber().integer().defined(), + visitors: yupNumber().integer().defined(), + revenue_cents: yupNumber().integer().defined(), +}).defined(); + +const SplitPointsSchema = yupArray(yupObject({ + date: yupString().defined(), + activity: yupNumber().defined(), +}).defined()).defined(); + +const ProjectRowSchema = yupObject({ + id: yupString().defined(), + display_name: yupString().defined(), + created_at: yupString().defined(), + total_users: yupNumber().integer().defined(), + verified_users: yupNumber().integer().defined(), + active_users: yupNumber().integer().defined(), + active_users_prev: yupNumber().integer().defined(), + signups: yupNumber().integer().defined(), + signups_prev: yupNumber().integer().defined(), + revenue_cents: yupNumber().integer().defined(), + revenue_cents_prev: yupNumber().integer().defined(), + features: yupArray(yupString().defined()).defined(), + sparkline: yupArray(yupNumber().defined()).defined(), +}).defined(); + +export const GET = createSmartRouteHandler({ + metadata: { hidden: true }, + request: yupObject({ + auth: yupObject({ + type: clientOrHigherAuthTypeSchema.defined(), + tenancy: adaptSchema.defined(), + user: adaptSchema, + project: adaptSchema.defined(), + }), + }), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["json"]).defined(), + body: yupObject({ + generated_at: yupString().defined(), + window_days: yupNumber().integer().defined(), + kpis: yupObject({ + active_projects: KpiSchema, + total_users: KpiSchema, + verified_users: KpiSchema, + mau: KpiSchema, + dau_avg: KpiSchema, + stickiness: KpiSchema, + new_signups: KpiSchema, + mrr_cents: KpiSchema, + active_subscriptions: KpiSchema, + email_deliverability_rate: KpiSchema, + }).defined(), + series: yupArray(SeriesPointSchema).defined(), + activity_split: yupObject({ + total: SplitPointsSchema, + new: SplitPointsSchema, + retained: SplitPointsSchema, + reactivated: SplitPointsSchema, + }).defined(), + breakdowns: yupObject({ + auth_methods: yupArray(yupObject({ + method: yupString().defined(), + count: yupNumber().integer().defined(), + }).defined()).defined(), + users_by_status: yupObject({ + verified: yupNumber().integer().defined(), + unverified: yupNumber().integer().defined(), + anonymous: yupNumber().integer().defined(), + }).defined(), + users_by_country: yupRecord(yupString().defined(), yupNumber().integer().defined()).defined(), + email: yupObject({ + sent: yupNumber().integer().defined(), + delivered: yupNumber().integer().defined(), + bounced: yupNumber().integer().defined(), + error: yupNumber().integer().defined(), + in_progress: yupNumber().integer().defined(), + }).defined(), + dead_click_rate: yupNumber().defined(), + }).defined(), + total_projects: yupNumber().integer().defined(), + feature_adoption: yupArray(yupObject({ + feature: yupString().defined(), + projects_using: yupNumber().integer().defined(), + }).defined()).defined(), + projects: yupArray(ProjectRowSchema).defined(), + }).defined(), + }), + handler: async (req) => { + if (!req.auth.user) { + throw new KnownErrors.UserAuthenticationRequired(); + } + if (req.auth.project.id !== INTERNAL_PROJECT_ID) { + throw new KnownErrors.ExpectedInternalProject(); + } + // Being signed into the internal project is not enough — this returns data + // across ALL customer projects, so require membership of the internal project's + // owning team (the platform team). + await ensurePlatformAdmin(req.auth.user); + + const now = new Date(); + const todayUtc = new Date(now); + todayUtc.setUTCHours(0, 0, 0, 0); + const windowStart = new Date(todayUtc.getTime() - (WINDOW_DAYS - 1) * ONE_DAY_MS); // first day shown + const priorStart = new Date(todayUtc.getTime() - (2 * WINDOW_DAYS - 1) * ONE_DAY_MS); + const untilExclusive = new Date(todayUtc.getTime() + ONE_DAY_MS); + const midParam = chDateTime(windowStart); // boundary between prior and current windows + const sinceParam = chDateTime(windowStart); + const priorSinceParam = chDateTime(priorStart); + const untilParam = chDateTime(untilExclusive); + + const branchId = DEFAULT_BRANCH_ID; + + // Ordered day axis for the visible window. + const windowDays: string[] = []; + for (let i = 0; i < WINDOW_DAYS; i += 1) { + windowDays.push(ymd(new Date(windowStart.getTime() + i * ONE_DAY_MS))); + } + + // All real customer projects (exclude the internal project itself). + const projectRows = await globalPrismaClient.project.findMany({ + where: { id: { not: INTERNAL_PROJECT_ID } }, + select: { id: true, displayName: true, createdAt: true }, + }); + const projectInfo = new Map(projectRows.map((p) => [p.id, p])); + + if (projectInfo.size === 0) { + return { + statusCode: 200 as const, + bodyType: "json" as const, + body: emptyBody(now), + }; + } + + const clickhouse = getClickhouseAdminClientForMetrics(); + const chQuery = async (query: string, params: Record): Promise => { + const result = await clickhouse.query({ query, query_params: params, format: "JSONEachRow" }); + return await result.json(); + }; + + const internalProjectId = INTERNAL_PROJECT_ID; + const userScope = `branch_id = {branchId:String} AND sync_is_deleted = 0`; + const customerUserScope = `${userScope} AND project_id != {internalProjectId:String}`; + const customerEventScope = `project_id != {internalProjectId:String}`; + const baseParams = { branchId, internalProjectId }; + const windowParams = { branchId, internalProjectId, since: sinceParam, until: untilParam }; + const twoWindowParams = { branchId, internalProjectId, priorSince: priorSinceParam, mid: midParam, until: untilParam }; + + let ch: { + dauSeries: Array<{ day: string, c: string | number }>, + pvSeries: Array<{ day: string, pv: string | number, visitors: string | number }>, + signupSeries: Array<{ day: string, c: string | number }>, + mauProjects: Array<{ mauCur: string | number, mauPrev: string | number, projCur: string | number, projPrev: string | number }>, + userCounts: Array<{ total: string | number, totalPrev: string | number, verified: string | number, verifiedPrev: string | number, anonymous: string | number }>, + country: Array<{ country_code: string, c: string | number }>, + deadClicks: Array<{ clicks: string | number, dead: string | number }>, + split: Array<{ day: string, total_count: string, new_count: string, retained_count: string, reactivated_count: string }>, + totalsByProject: CountRow[], + verifiedByProject: CountRow[], + signupsByProject: Array<{ projectId: string, cur: string | number, prev: string | number }>, + activeByProject: Array<{ projectId: string, cur: string | number, prev: string | number }>, + sparkByProject: Array<{ projectId: string, day: string, c: string | number }>, + teamsByProject: CountRow[], + oauthByProject: CountRow[], + emailsByProject: CountRow[], + analyticsByProject: CountRow[], + }; + try { + const verifiedSubquery = ` + (project_id, id) IN ( + SELECT project_id, user_id FROM analytics_internal.contact_channels FINAL + WHERE branch_id = {branchId:String} AND sync_is_deleted = 0 AND type = 'EMAIL' AND is_verified = 1 + )`; + const [ + dauSeries, pvSeries, signupSeries, mauProjects, userCounts, country, deadClicks, split, + totalsByProject, verifiedByProject, signupsByProject, activeByProject, sparkByProject, + teamsByProject, oauthByProject, emailsByProject, analyticsByProject, + ] = await Promise.all([ + // Platform daily DAU (active users) over the visible window. + chQuery<{ day: string, c: string | number }>(` + SELECT toDate(event_at) AS day, uniqExact(assumeNotNull(user_id)) AS c + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + GROUP BY day ORDER BY day ASC + `, windowParams), + // Page views + unique visitors per day. + chQuery<{ day: string, pv: string | number, visitors: string | number }>(` + SELECT toDate(event_at) AS day, + countIf(event_type = '$page-view') AS pv, + uniqExactIf(assumeNotNull(user_id), event_type = '$page-view') AS visitors + FROM analytics_internal.events + WHERE event_type IN ('$page-view', '$click') + AND ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + GROUP BY day ORDER BY day ASC + `, windowParams), + // Signups per day (users table). + chQuery<{ day: string, c: string | number }>(` + SELECT toDate(signed_up_at, 'UTC') AS day, count() AS c + FROM analytics_internal.users FINAL + WHERE ${customerUserScope} AND is_anonymous = 0 + AND signed_up_at >= {since:DateTime} AND signed_up_at < {until:DateTime} + GROUP BY day ORDER BY day ASC + `, windowParams), + // MAU + active projects, current vs prior 30d window (single pass over 60d). + chQuery<{ mauCur: string | number, mauPrev: string | number, projCur: string | number, projPrev: string | number }>(` + SELECT + uniqExactIf(assumeNotNull(user_id), event_at >= {mid:DateTime}) AS mauCur, + uniqExactIf(assumeNotNull(user_id), event_at < {mid:DateTime}) AS mauPrev, + uniqExactIf(project_id, event_at >= {mid:DateTime}) AS projCur, + uniqExactIf(project_id, event_at < {mid:DateTime}) AS projPrev + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {priorSince:DateTime} AND event_at < {until:DateTime} + `, twoWindowParams), + // User stock counts: total, verified, anonymous (now + as-of window start). + chQuery<{ total: string | number, totalPrev: string | number, verified: string | number, verifiedPrev: string | number, anonymous: string | number }>(` + SELECT + countIf(is_anonymous = 0) AS total, + countIf(is_anonymous = 0 AND signed_up_at < {mid:DateTime}) AS totalPrev, + countIf(is_anonymous = 0 AND ${verifiedSubquery}) AS verified, + countIf(is_anonymous = 0 AND signed_up_at < {mid:DateTime} AND ${verifiedSubquery}) AS verifiedPrev, + countIf(is_anonymous = 1) AS anonymous + FROM analytics_internal.users FINAL + WHERE ${customerUserScope} + `, { branchId, internalProjectId, mid: midParam }), + // Users by country (for the globe) over the window. + chQuery<{ country_code: string, c: string | number }>(` + SELECT country_code, count() AS c FROM ( + SELECT user_id, argMax(cc, event_at) AS country_code FROM ( + SELECT user_id, event_at, CAST(data.ip_info.country_code, 'Nullable(String)') AS cc + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + ) WHERE cc IS NOT NULL GROUP BY user_id + ) WHERE country_code IS NOT NULL GROUP BY country_code ORDER BY c DESC + `, windowParams), + // Dead-click health over the window. + chQuery<{ clicks: string | number, dead: string | number }>(` + SELECT count() AS clicks, sum(is_dead) AS dead + FROM analytics_internal.clickmap_events + WHERE ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + `, windowParams), + // New / retained / reactivated split across all projects. + chQuery<{ day: string, total_count: string, new_count: string, retained_count: string, reactivated_count: string }>(` + SELECT + toString(w.day) AS day, + count() AS total_count, + countIf(f.first_date = w.day) AS new_count, + countIf(f.first_date < w.day AND w.prev_day = addDays(w.day, -1)) AS retained_count, + countIf(f.first_date < w.day AND (isNull(w.prev_day) OR w.prev_day < addDays(w.day, -1))) AS reactivated_count + FROM ( + SELECT day, entity_id, lagInFrame(day, 1) OVER (PARTITION BY entity_id ORDER BY day) AS prev_day + FROM ( + SELECT DISTINCT toDate(event_at) AS day, assumeNotNull(user_id) AS entity_id + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 + ) + ) AS w + LEFT JOIN ( + SELECT assumeNotNull(user_id) AS entity_id, toDate(min(event_at)) AS first_date + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at < {until:DateTime} + AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 + GROUP BY entity_id + ) AS f USING (entity_id) + GROUP BY w.day ORDER BY w.day ASC + `, windowParams), + // Per-project total users. + chQuery(` + SELECT project_id AS projectId, count() AS c + FROM analytics_internal.users FINAL + WHERE ${customerUserScope} AND is_anonymous = 0 GROUP BY project_id + `, baseParams), + // Per-project verified users. + chQuery(` + SELECT project_id AS projectId, count() AS c + FROM analytics_internal.users FINAL + WHERE ${customerUserScope} AND is_anonymous = 0 AND ${verifiedSubquery} GROUP BY project_id + `, baseParams), + // Per-project signups, current vs prior window. + chQuery<{ projectId: string, cur: string | number, prev: string | number }>(` + SELECT project_id AS projectId, + countIf(signed_up_at >= {mid:DateTime}) AS cur, + countIf(signed_up_at < {mid:DateTime}) AS prev + FROM analytics_internal.users FINAL + WHERE ${customerUserScope} AND is_anonymous = 0 + AND signed_up_at >= {priorSince:DateTime} AND signed_up_at < {until:DateTime} + GROUP BY project_id + `, twoWindowParams), + // Per-project active users, current vs prior window. + chQuery<{ projectId: string, cur: string | number, prev: string | number }>(` + SELECT project_id AS projectId, + uniqExactIf(assumeNotNull(user_id), event_at >= {mid:DateTime}) AS cur, + uniqExactIf(assumeNotNull(user_id), event_at < {mid:DateTime}) AS prev + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {priorSince:DateTime} AND event_at < {until:DateTime} + GROUP BY project_id + `, twoWindowParams), + // Per-project daily active sparkline (visible window). + chQuery<{ projectId: string, day: string, c: string | number }>(` + SELECT project_id AS projectId, toDate(event_at) AS day, uniqExact(assumeNotNull(user_id)) AS c + FROM analytics_internal.events + WHERE event_type = '$token-refresh' AND user_id IS NOT NULL + AND ${customerEventScope} + AND event_at >= {since:DateTime} AND event_at < {until:DateTime} + GROUP BY project_id, day + `, windowParams), + // Feature adoption signals (per project) from synced CH tables. + chQuery(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.teams FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams), + chQuery(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.connected_accounts FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams), + chQuery(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.email_outboxes FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams), + chQuery(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.events WHERE event_type = '$page-view' AND branch_id = {branchId:String} AND ${customerEventScope} GROUP BY project_id`, baseParams), + ]); + ch = { + dauSeries, pvSeries, signupSeries, mauProjects, userCounts, country, deadClicks, split, + totalsByProject, verifiedByProject, signupsByProject, activeByProject, sparkByProject, + teamsByProject, oauthByProject, emailsByProject, analyticsByProject, + }; + } catch (cause) { + throw new HexclaveAssertionError(`Failed to load platform analytics from ClickHouse: ${cause instanceof Error ? cause.message : String(cause)}`, { + cause, userId: req.auth.user.id, + }); + } + + // Postgres-only signals: revenue (per project + daily), MRR (true recurring), + // auth-method split, email deliverability, payments/replay adoption. + let pg: { + revenueDaily: Array<{ day: string, cents: string | number }>, + revenueByProject: Array<{ projectId: string, cur: string | number, prev: string | number }>, + subscriptions: Array<{ projectId: string, product: unknown, priceId: string | null, quantity: number }>, + authMethods: Array<{ method: string, count: number }>, + email: Array<{ sent: number, delivered: number, bounced: number, error: number, in_progress: number, deliveredCur: number, finishedCur: number, deliveredPrev: number, finishedPrev: number }>, + paymentsRows: Array<{ projectId: string }>, + replayRows: Array<{ projectId: string }>, + }; + try { + const replica = globalPrismaClient.$replica(); + const since = windowStart; + const prior = priorStart; + const mid = windowStart; + const [revenueDaily, revenueByProject, subscriptions, authMethods, email, paymentsRows, replayRows] = await Promise.all([ + replica.$queryRaw>(Prisma.sql` + SELECT TO_CHAR(si."createdAt"::date, 'YYYY-MM-DD') AS day, COALESCE(SUM(si."amountTotal"), 0)::bigint AS cents + FROM "SubscriptionInvoice" si JOIN "Tenancy" t ON t."id" = si."tenancyId" + WHERE si."amountTotal" IS NOT NULL AND si."status" = ANY(${REVENUE_INVOICE_STATUSES}) + AND si."createdAt" >= ${since} AND t."projectId" <> ${INTERNAL_PROJECT_ID} + GROUP BY day ORDER BY day + `), + replica.$queryRaw>(Prisma.sql` + SELECT t."projectId" AS "projectId", + COALESCE(SUM("amountTotal") FILTER (WHERE si."createdAt" >= ${mid}), 0)::bigint AS cur, + COALESCE(SUM("amountTotal") FILTER (WHERE si."createdAt" < ${mid}), 0)::bigint AS prev + FROM "SubscriptionInvoice" si JOIN "Tenancy" t ON t."id" = si."tenancyId" + WHERE si."amountTotal" IS NOT NULL AND si."status" = ANY(${REVENUE_INVOICE_STATUSES}) + AND si."createdAt" >= ${prior} AND t."projectId" <> ${INTERNAL_PROJECT_ID} + GROUP BY t."projectId" + `), + replica.$queryRaw>(Prisma.sql` + SELECT t."projectId" AS "projectId", s."product" AS product, s."priceId" AS "priceId", s."quantity" AS quantity + FROM "Subscription" s JOIN "Tenancy" t ON t."id" = s."tenancyId" + WHERE s."status"::text = ANY(${MRR_SUBSCRIPTION_STATUSES}) AND t."projectId" <> ${INTERNAL_PROJECT_ID} + `), + replica.$queryRaw>(Prisma.sql` + SELECT method, COUNT(*)::int AS count FROM ( + SELECT COALESCE( + oaam."configOAuthProviderId"::text, + CASE WHEN pam."authMethodId" IS NOT NULL THEN 'password' END, + CASE WHEN pkm."authMethodId" IS NOT NULL THEN 'passkey' END, + CASE WHEN oam."authMethodId" IS NOT NULL THEN 'otp' END, + 'other' + ) AS method + FROM "AuthMethod" am + JOIN "Tenancy" t ON t."id" = am."tenancyId" + LEFT JOIN "OAuthAuthMethod" oaam ON oaam."tenancyId" = am."tenancyId" AND oaam."authMethodId" = am."id" + LEFT JOIN "PasswordAuthMethod" pam ON pam."tenancyId" = am."tenancyId" AND pam."authMethodId" = am."id" + LEFT JOIN "PasskeyAuthMethod" pkm ON pkm."tenancyId" = am."tenancyId" AND pkm."authMethodId" = am."id" + LEFT JOIN "OtpAuthMethod" oam ON oam."tenancyId" = am."tenancyId" AND oam."authMethodId" = am."id" + WHERE t."projectId" <> ${INTERNAL_PROJECT_ID} + ) sub GROUP BY method ORDER BY count DESC + `), + replica.$queryRaw>(Prisma.sql` + SELECT + COUNT(*) FILTER (WHERE eo."finishedSendingAt" IS NOT NULL)::int AS sent, + COUNT(*) FILTER (WHERE eo."deliveredAt" IS NOT NULL)::int AS delivered, + COUNT(*) FILTER (WHERE eo."bouncedAt" IS NOT NULL)::int AS bounced, + COUNT(*) FILTER (WHERE eo."simpleStatus"::text = 'ERROR')::int AS error, + COUNT(*) FILTER (WHERE eo."simpleStatus"::text = 'IN_PROGRESS')::int AS in_progress, + COUNT(*) FILTER (WHERE eo."deliveredAt" IS NOT NULL AND eo."createdAt" >= ${mid})::int AS "deliveredCur", + COUNT(*) FILTER (WHERE eo."finishedSendingAt" IS NOT NULL AND eo."createdAt" >= ${mid})::int AS "finishedCur", + COUNT(*) FILTER (WHERE eo."deliveredAt" IS NOT NULL AND eo."createdAt" >= ${prior} AND eo."createdAt" < ${mid})::int AS "deliveredPrev", + COUNT(*) FILTER (WHERE eo."finishedSendingAt" IS NOT NULL AND eo."createdAt" >= ${prior} AND eo."createdAt" < ${mid})::int AS "finishedPrev" + FROM "EmailOutbox" eo JOIN "Tenancy" t ON t."id" = eo."tenancyId" + WHERE t."projectId" <> ${INTERNAL_PROJECT_ID} + `), + replica.$queryRaw>(Prisma.sql` + SELECT DISTINCT t."projectId" AS "projectId" + FROM "Subscription" s JOIN "Tenancy" t ON t."id" = s."tenancyId" + WHERE s."status" IN ('active', 'trialing', 'paused') AND t."projectId" <> ${INTERNAL_PROJECT_ID} + `), + replica.$queryRaw>(Prisma.sql` + SELECT DISTINCT t."projectId" AS "projectId" + FROM "SessionReplay" sr JOIN "Tenancy" t ON t."id" = sr."tenancyId" + WHERE t."projectId" <> ${INTERNAL_PROJECT_ID} + `), + ]); + pg = { revenueDaily, revenueByProject, subscriptions, authMethods, email, paymentsRows, replayRows }; + } catch (cause) { + throw new HexclaveAssertionError(`Failed to load platform analytics from Postgres: ${cause instanceof Error ? cause.message : String(cause)}`, { + cause, userId: req.auth.user.id, + }); + } + + // ---- Assemble series ---- + const dauByDay = new Map(ch.dauSeries.map((r) => [r.day.split("T")[0], num(r.c)])); + const pvByDay = new Map(ch.pvSeries.map((r) => [r.day.split("T")[0], { pv: num(r.pv), visitors: num(r.visitors) }])); + const signupByDay = new Map(ch.signupSeries.map((r) => [r.day.split("T")[0], num(r.c)])); + const revenueByDay = new Map(pg.revenueDaily.map((r) => [r.day, num(r.cents)])); + const series = windowDays.map((date) => ({ + date, + signups: signupByDay.get(date) ?? 0, + active_users: dauByDay.get(date) ?? 0, + page_views: pvByDay.get(date)?.pv ?? 0, + visitors: pvByDay.get(date)?.visitors ?? 0, + revenue_cents: revenueByDay.get(date) ?? 0, + })); + + // ---- Activity split ---- + const splitByDay = new Map(ch.split.map((r) => [r.day.split("T")[0], r])); + const splitField = (field: "total_count" | "new_count" | "retained_count" | "reactivated_count") => + windowDays.map((date) => ({ date, activity: num(splitByDay.get(date)?.[field]) })); + const activity_split = { + total: splitField("total_count"), + new: splitField("new_count"), + retained: splitField("retained_count"), + reactivated: splitField("reactivated_count"), + }; + + // ---- KPIs ---- + const mp = ch.mauProjects[0] ?? { mauCur: 0, mauPrev: 0, projCur: 0, projPrev: 0 }; + const uc = ch.userCounts[0] ?? { total: 0, totalPrev: 0, verified: 0, verifiedPrev: 0, anonymous: 0 }; + const dauAvgCur = Math.round(series.reduce((s, p) => s + p.active_users, 0) / Math.max(1, WINDOW_DAYS)); + const mauCur = num(mp.mauCur); + const mauPrev = num(mp.mauPrev); + const stick = (dau: number, mau: number) => mau > 0 ? Number(((dau / mau) * 100).toFixed(1)) : 0; + const signupsCur = series.reduce((s, p) => s + p.signups, 0); + const emailRow = pg.email[0] ?? { sent: 0, delivered: 0, bounced: 0, error: 0, in_progress: 0, deliveredCur: 0, finishedCur: 0, deliveredPrev: 0, finishedPrev: 0 }; + const rate = (n: number, d: number) => d > 0 ? Number(((n / d) * 100).toFixed(1)) : 0; + + // MRR (true recurring, normalized to monthly cents). + let mrrCents = 0; + for (const s of pg.subscriptions) { + mrrCents += monthlyRecurringCents(s.product, s.priceId, num(s.quantity)); + } + + const kpis = { + active_projects: { value: num(mp.projCur), prev: num(mp.projPrev) }, + total_users: { value: num(uc.total), prev: num(uc.totalPrev) }, + verified_users: { value: num(uc.verified), prev: num(uc.verifiedPrev) }, + mau: { value: mauCur, prev: mauPrev }, + dau_avg: { value: dauAvgCur, prev: null }, + stickiness: { value: stick(dauAvgCur, mauCur), prev: null }, + new_signups: { value: signupsCur, prev: null }, + mrr_cents: { value: mrrCents, prev: null }, + active_subscriptions: { value: pg.subscriptions.length, prev: null }, + email_deliverability_rate: { + value: rate(emailRow.deliveredCur, emailRow.finishedCur), + prev: emailRow.finishedPrev > 0 ? rate(emailRow.deliveredPrev, emailRow.finishedPrev) : null, + }, + }; + + // ---- Breakdowns ---- + const usersByCountryMap = new Map(); + for (const r of ch.country) { + if (r.country_code) usersByCountryMap.set(r.country_code.toUpperCase(), num(r.c)); + } + const usersByCountry = Object.fromEntries(usersByCountryMap); + const nonAnon = num(uc.total); + const verified = num(uc.verified); + const breakdowns = { + auth_methods: pg.authMethods.map((m) => ({ method: m.method, count: num(m.count) })).filter((m) => m.count > 0), + users_by_status: { + verified, + unverified: Math.max(0, nonAnon - verified), + anonymous: num(uc.anonymous), + }, + users_by_country: usersByCountry, + email: { + sent: emailRow.sent, + delivered: emailRow.delivered, + bounced: emailRow.bounced, + error: emailRow.error, + in_progress: emailRow.in_progress, + }, + dead_click_rate: rate(num(ch.deadClicks[0]?.dead), num(ch.deadClicks[0]?.clicks)), + }; + + // ---- Feature adoption ---- + const keysWithCount = (rows: CountRow[]) => rowsToMap(rows); + const countProjects = (map: Map) => { + let n = 0; + for (const [id, c] of map) if (c > 0 && projectInfo.has(id) && id !== INTERNAL_PROJECT_ID) n += 1; + return n; + }; + const countList = (ids: Iterable) => { + const seen = new Set(); + for (const id of ids) if (projectInfo.has(id) && id !== INTERNAL_PROJECT_ID) seen.add(id); + return seen.size; + }; + const teamsMap = keysWithCount(ch.teamsByProject); + const oauthMap = keysWithCount(ch.oauthByProject); + const emailsMap = keysWithCount(ch.emailsByProject); + const analyticsMap = keysWithCount(ch.analyticsByProject); + const feature_adoption = [ + { feature: "teams", projects_using: countProjects(teamsMap) }, + { feature: "oauth", projects_using: countProjects(oauthMap) }, + { feature: "emails", projects_using: countProjects(emailsMap) }, + { feature: "analytics", projects_using: countProjects(analyticsMap) }, + { feature: "payments", projects_using: countList(pg.paymentsRows.map((r) => r.projectId)) }, + { feature: "session_replay", projects_using: countList(pg.replayRows.map((r) => r.projectId)) }, + ]; + + // ---- Per-project leaderboard ---- + const totalsMap = rowsToMap(ch.totalsByProject); + const verifiedMap = rowsToMap(ch.verifiedByProject); + const signupsMap = new Map(ch.signupsByProject.map((r) => [r.projectId, { cur: num(r.cur), prev: num(r.prev) }])); + const activeMap = new Map(ch.activeByProject.map((r) => [r.projectId, { cur: num(r.cur), prev: num(r.prev) }])); + const revenueMap = new Map(pg.revenueByProject.map((r) => [r.projectId, { cur: num(r.cur), prev: num(r.prev) }])); + const sparkIndex = new Map>(); + for (const r of ch.sparkByProject) { + const day = r.day.split("T")[0]; + let m = sparkIndex.get(r.projectId); + if (!m) { + m = new Map(); + sparkIndex.set(r.projectId, m); + } + m.set(day, num(r.c)); + } + const featureSet = (id: string): string[] => { + const f: string[] = []; + if ((teamsMap.get(id) ?? 0) > 0) f.push("teams"); + if ((oauthMap.get(id) ?? 0) > 0) f.push("oauth"); + if ((emailsMap.get(id) ?? 0) > 0) f.push("emails"); + if ((analyticsMap.get(id) ?? 0) > 0) f.push("analytics"); + return f; + }; + const paymentsSet = new Set(pg.paymentsRows.map((r) => r.projectId)); + const replaySet = new Set(pg.replayRows.map((r) => r.projectId)); + + const projects = projectRows.map((p) => { + const sp = sparkIndex.get(p.id); + const features = featureSet(p.id); + if (paymentsSet.has(p.id)) features.push("payments"); + if (replaySet.has(p.id)) features.push("session_replay"); + return { + id: p.id, + display_name: p.displayName, + created_at: p.createdAt.toISOString(), + total_users: totalsMap.get(p.id) ?? 0, + verified_users: verifiedMap.get(p.id) ?? 0, + active_users: activeMap.get(p.id)?.cur ?? 0, + active_users_prev: activeMap.get(p.id)?.prev ?? 0, + signups: signupsMap.get(p.id)?.cur ?? 0, + signups_prev: signupsMap.get(p.id)?.prev ?? 0, + revenue_cents: revenueMap.get(p.id)?.cur ?? 0, + revenue_cents_prev: revenueMap.get(p.id)?.prev ?? 0, + features, + sparkline: windowDays.map((d) => sp?.get(d) ?? 0), + }; + }); + projects.sort((a, b) => b.total_users - a.total_users || b.active_users - a.active_users); + + return { + statusCode: 200 as const, + bodyType: "json" as const, + body: { + generated_at: now.toISOString(), + window_days: WINDOW_DAYS, + kpis, + series, + activity_split, + breakdowns, + total_projects: projectRows.length, + feature_adoption, + projects: projects.slice(0, LEADERBOARD_LIMIT), + }, + }; + }, +}); + +function emptyBody(now: Date) { + const zeroKpi = { value: 0, prev: null }; + return { + generated_at: now.toISOString(), + window_days: WINDOW_DAYS, + kpis: { + active_projects: zeroKpi, + total_users: zeroKpi, + verified_users: zeroKpi, + mau: zeroKpi, + dau_avg: zeroKpi, + stickiness: zeroKpi, + new_signups: zeroKpi, + mrr_cents: zeroKpi, + active_subscriptions: zeroKpi, + email_deliverability_rate: zeroKpi, + }, + series: [], + activity_split: { total: [], new: [], retained: [], reactivated: [] }, + breakdowns: { + auth_methods: [], + users_by_status: { verified: 0, unverified: 0, anonymous: 0 }, + users_by_country: {}, + email: { sent: 0, delivered: 0, bounced: 0, error: 0, in_progress: 0 }, + dead_click_rate: 0, + }, + total_projects: 0, + feature_adoption: [], + projects: [], + }; +} diff --git a/apps/backend/src/lib/platform-admin.test.ts b/apps/backend/src/lib/platform-admin.test.ts new file mode 100644 index 000000000..4e010a354 --- /dev/null +++ b/apps/backend/src/lib/platform-admin.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it, vi } from "vitest"; +import { ensurePlatformAdmin, isPlatformAdmin } from "./platform-admin"; +import * as projects from "./projects"; + +vi.mock("./projects", () => ({ + listManagedProjectIds: vi.fn(), +})); + +const mockListManagedProjectIds = vi.mocked(projects.listManagedProjectIds); + +// The actual user object is only forwarded to listManagedProjectIds, which is +// mocked, so the concrete shape doesn't matter. UsersCrud["Admin"]["Read"] is a +// large generated type; building a full fixture adds noise without value here. +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- see above +const fakeUser: Parameters[0] = { id: "user-1" } as any; + +describe("isPlatformAdmin", () => { + it("returns true when user manages the internal project", async () => { + mockListManagedProjectIds.mockResolvedValue(["internal", "other-project"]); + await expect(isPlatformAdmin(fakeUser)).resolves.toBe(true); + expect(mockListManagedProjectIds).toHaveBeenCalledWith(fakeUser); + }); + + it("returns false when user does not manage the internal project", async () => { + mockListManagedProjectIds.mockResolvedValue(["some-project", "another-project"]); + await expect(isPlatformAdmin(fakeUser)).resolves.toBe(false); + }); + + it("returns false when user manages no projects", async () => { + mockListManagedProjectIds.mockResolvedValue([]); + await expect(isPlatformAdmin(fakeUser)).resolves.toBe(false); + }); +}); + +describe("ensurePlatformAdmin", () => { + it("resolves without throwing for platform admins", async () => { + mockListManagedProjectIds.mockResolvedValue(["internal"]); + await expect(ensurePlatformAdmin(fakeUser)).resolves.toBeUndefined(); + }); + + it("throws a 403 StatusError for non-platform-admins", async () => { + mockListManagedProjectIds.mockResolvedValue(["customer-project"]); + await expect(ensurePlatformAdmin(fakeUser)).rejects.toMatchInlineSnapshot( + `[StatusError: You do not have access to platform analytics.]` + ); + }); + + it("throws a 403 StatusError when user manages no projects at all", async () => { + mockListManagedProjectIds.mockResolvedValue([]); + await expect(ensurePlatformAdmin(fakeUser)).rejects.toMatchInlineSnapshot( + `[StatusError: You do not have access to platform analytics.]` + ); + }); +}); diff --git a/apps/backend/src/lib/platform-admin.ts b/apps/backend/src/lib/platform-admin.ts new file mode 100644 index 000000000..9fe688221 --- /dev/null +++ b/apps/backend/src/lib/platform-admin.ts @@ -0,0 +1,25 @@ +import { UsersCrud } from "@hexclave/shared/dist/interface/crud/users"; +import { StatusError } from "@hexclave/shared/dist/utils/errors"; +import { listManagedProjectIds } from "./projects"; + +// Authorization for platform-wide (cross-customer) internal endpoints. +// +// Being a signed-in user of the "internal" project is NOT sufficient: the +// internal project's publishable client key is public and, on deployments with +// open dashboard sign-up, anyone can create an internal-project account (in their +// own team). Access is therefore gated on membership of the team that OWNS the +// internal project — i.e. the platform team. That is exactly what +// `listManagedProjectIds` encodes (a user manages a project when they belong to +// its owner team), so the internal project appears in that list only for platform +// team members. + +export async function isPlatformAdmin(user: UsersCrud["Admin"]["Read"]): Promise { + const managedProjectIds = await listManagedProjectIds(user); + return managedProjectIds.includes("internal"); +} + +export async function ensurePlatformAdmin(user: UsersCrud["Admin"]["Read"]): Promise { + if (!(await isPlatformAdmin(user))) { + throw new StatusError(403, "You do not have access to platform analytics."); + } +} diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 3991215ca..9f78ffe07 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -92,7 +92,7 @@ "libsodium-wrappers": "^0.8.2", "lodash": "^4.17.21", "motion": "^12.39.0", - "next": "16.2.7", + "next": "16.2.9", "next-themes": "^0.2.1", "posthog-js": "^1.336.1", "qrcode": "^1.5.4", diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page-client.tsx new file mode 100644 index 000000000..8ba4815dc --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page-client.tsx @@ -0,0 +1,653 @@ +'use client'; + +import { Skeleton, Typography } from "@/components/ui"; +import { Card, CardContent } from "@/components/ui/card"; +import { DesignAnalyticsCard } from "@/components/design-components"; +import { hexclaveAppInternalsSymbol } from "@/lib/hexclave-app-internals"; +import { cn } from "@/lib/utils"; +import { + ChartLineUpIcon, + CreditCardIcon, + CursorClickIcon, + EnvelopeSimpleIcon, + FingerprintSimpleIcon, + LockKeyIcon, + MonitorPlayIcon, + UsersThreeIcon, +} from "@phosphor-icons/react"; +import { useStackApp, useUser } from "@hexclave/next"; +import { captureError } from "@hexclave/shared/dist/utils/errors"; +import { runAsynchronously } from "@hexclave/shared/dist/utils/promises"; +import { useEffect, useMemo, useState } from "react"; +import { PageLayout } from "../page-layout"; +import { useProjectId } from "../use-admin-app"; +import { + ComposedAnalyticsChart, + DonutChartDisplay, + StackedBarChartDisplay, + type ComposedDataPoint, + type StackedDataPoint, +} from "../(overview)/line-chart"; +import { GlobeSection } from "../(overview)/globe"; + +type Kpi = { value: number, prev: number | null }; +type SeriesPoint = { + date: string, + signups: number, + active_users: number, + page_views: number, + visitors: number, + revenue_cents: number, +}; +type SplitPoint = { date: string, activity: number }; +type ProjectRow = { + id: string, + display_name: string, + created_at: string, + total_users: number, + verified_users: number, + active_users: number, + active_users_prev: number, + signups: number, + signups_prev: number, + revenue_cents: number, + revenue_cents_prev: number, + features: string[], + sparkline: number[], +}; +type PlatformAnalytics = { + generated_at: string, + window_days: number, + kpis: { + active_projects: Kpi, + total_users: Kpi, + verified_users: Kpi, + mau: Kpi, + dau_avg: Kpi, + stickiness: Kpi, + new_signups: Kpi, + mrr_cents: Kpi, + active_subscriptions: Kpi, + email_deliverability_rate: Kpi, + }, + series: SeriesPoint[], + activity_split: { total: SplitPoint[], new: SplitPoint[], retained: SplitPoint[], reactivated: SplitPoint[] }, + breakdowns: { + auth_methods: Array<{ method: string, count: number }>, + users_by_status: { verified: number, unverified: number, anonymous: number }, + users_by_country: Record, + email: { sent: number, delivered: number, bounced: number, error: number, in_progress: number }, + dead_click_rate: number, + }, + total_projects: number, + feature_adoption: Array<{ feature: string, projects_using: number }>, + projects: ProjectRow[], +}; + +type LoadState = + | { status: "loading" } + | { status: "forbidden" } + | { status: "error" } + | { status: "ok", data: PlatformAnalytics }; + +type HexclaveAppInternals = { + sendRequest: (path: string, requestOptions: RequestInit, requestType?: "client" | "server" | "admin") => Promise, +}; + +function getStackAppInternals(appValue: unknown): HexclaveAppInternals { + if (appValue == null || typeof appValue !== "object") { + throw new Error("The Stack app instance is unavailable."); + } + const internals = Reflect.get(appValue, hexclaveAppInternalsSymbol); + if ( + internals == null || + typeof internals !== "object" || + !("sendRequest" in internals) || + typeof (internals as HexclaveAppInternals).sendRequest !== "function" + ) { + throw new Error("The Stack client app cannot send internal requests."); + } + return internals as HexclaveAppInternals; +} + +function formatNumber(n: number): string { + return Math.round(n).toLocaleString(); +} + +function formatCompact(n: number): string { + if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (Math.abs(n) >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + return formatNumber(n); +} + +function formatUsdFromCents(cents: number): string { + const dollars = cents / 100; + if (Math.abs(dollars) >= 1_000) return `$${(dollars / 1_000).toFixed(1)}k`; + return `$${dollars.toLocaleString(undefined, { maximumFractionDigits: 0 })}`; +} + +function growthPct(value: number, prev: number): number | null { + if (prev === 0) return value === 0 ? 0 : null; + return Number((((value - prev) / prev) * 100).toFixed(1)); +} + +export default function PageClient() { + const projectId = useProjectId(); + useUser({ or: "redirect", projectIdMustMatch: "internal" }); + + if (projectId !== "internal") { + return null; + } + + return ( + + + + ); +} + +function PlatformAnalyticsContent() { + const app = useStackApp(); + const appInternals = useMemo(() => getStackAppInternals(app), [app]); + const [state, setState] = useState({ status: "loading" }); + const [range, setRange] = useState<7 | 30>(30); + + useEffect(() => { + let cancelled = false; + runAsynchronously(async () => { + setState({ status: "loading" }); + try { + const response = await appInternals.sendRequest("/internal/platform-analytics", {}, "client"); + if (response.status === 403) { + if (!cancelled) setState({ status: "forbidden" }); + return; + } + if (!response.ok) { + throw new Error(`Failed to load platform analytics: ${response.status} ${await response.text()}`); + } + const body = await response.json() as unknown; + if (body == null || typeof body !== "object" || !Array.isArray((body as PlatformAnalytics).projects)) { + throw new Error("Platform analytics endpoint returned an invalid response."); + } + if (!cancelled) setState({ status: "ok", data: body as PlatformAnalytics }); + } catch (e) { + if (cancelled) return; + setState({ status: "error" }); + captureError("platform-analytics-load", e); + } + }); + return () => { + cancelled = true; + }; + }, [appInternals]); + + if (state.status === "loading") { + return ( +
+
+ {Array.from({ length: 5 }).map((_, i) => )} +
+ + +
+ ); + } + + if (state.status === "forbidden") { + return ( + + + + Access restricted + + Platform analytics is limited to members of the internal project's team. + + + + ); + } + + if (state.status === "error") { + return ( + + + Could not load platform analytics. Please try again. + + + ); + } + + return ; +} + +function Dashboard({ + data, + range, + onRangeChange, +}: { + data: PlatformAnalytics, + range: 7 | 30, + onRangeChange: (range: 7 | 30) => void, +}) { + const slice = (arr: T[]): T[] => (range === 30 ? arr : arr.slice(-range)); + const series = slice(data.series); + + const composed: ComposedDataPoint[] = series.map((p) => ({ + date: p.date, + new_cents: p.revenue_cents, + refund_cents: 0, + page_views: p.page_views, + visitors: p.visitors, + dau: p.active_users, + })); + + const splitByDate = new Map(); + for (const p of data.activity_split.total) { + splitByDate.set(p.date, { date: p.date, new: 0, retained: 0, reactivated: 0 }); + } + for (const p of data.activity_split.new) { + const d = splitByDate.get(p.date); + if (d) d.new = p.activity; + } + for (const p of data.activity_split.retained) { + const d = splitByDate.get(p.date); + if (d) d.retained = p.activity; + } + for (const p of data.activity_split.reactivated) { + const d = splitByDate.get(p.date); + if (d) d.reactivated = p.activity; + } + const stacked = slice([...splitByDate.values()]); + + const k = data.kpis; + const tiles = [ + { label: "Active projects", kpi: k.active_projects, format: formatNumber }, + { label: "Total users", kpi: k.total_users, format: formatCompact }, + { label: "Verified users", kpi: k.verified_users, format: formatCompact }, + { label: "MAU", kpi: k.mau, format: formatCompact }, + { label: "Stickiness", kpi: k.stickiness, format: (n: number) => `${n}%`, suffixDelta: "pp" }, + { label: `New sign-ups (${data.window_days}d)`, kpi: k.new_signups, format: formatCompact }, + { label: "MRR", kpi: k.mrr_cents, format: formatUsdFromCents }, + { label: "Active subscriptions", kpi: k.active_subscriptions, format: formatNumber }, + { label: "Email deliverability", kpi: k.email_deliverability_rate, format: (n: number) => `${n}%`, suffixDelta: "pp" }, + { label: `Avg DAU (${data.window_days}d)`, kpi: k.dau_avg, format: formatCompact }, + ]; + + return ( +
+
+ +
+ +
+ {tiles.map((tile) => ( + + ))} +
+ +
+ + {composed.length === 0 + ? + : } + + + {stacked.length === 0 + ? + : } + +
+ + + +
+ +
+ +
+
+ + {data.breakdowns.auth_methods.length === 0 + ? + : } + + + + +
+ +
+ + + +
+
+ ); +} + +function RangeToggle({ range, onRangeChange }: { range: 7 | 30, onRangeChange: (range: 7 | 30) => void }) { + return ( +
+ {([7, 30] as const).map((value) => ( + + ))} +
+ ); +} + +function KpiTile({ + label, + kpi, + format, + suffixDelta, +}: { + label: string, + kpi: Kpi, + format: (n: number) => string, + suffixDelta?: string, +}) { + const delta = kpi.prev == null ? null : suffixDelta === "pp" + ? Number((kpi.value - kpi.prev).toFixed(1)) + : growthPct(kpi.value, kpi.prev); + return ( + + + {label} +
+ {format(kpi.value)} + {delta != null && delta !== 0 && ( + 0 ? "text-emerald-500 dark:text-emerald-400" : "text-red-500 dark:text-red-400", + )}> + {delta > 0 ? "+" : ""}{delta}{suffixDelta === "pp" ? "pp" : "%"} + + )} +
+
+
+ ); +} + +function ChartCard({ + title, + subtitle, + className, + gradient, + children, +}: { + title: string, + subtitle?: string, + className?: string, + gradient: "blue" | "cyan" | "purple" | "green" | "orange" | "slate", + children: React.ReactNode, +}) { + return ( + +
+ {title} + {subtitle && {subtitle}} +
+
{children}
+
+ ); +} + +function EmptyChart() { + return ( +
+ No data for this period. +
+ ); +} + +const STATUS_STYLES: Record = { + Growing: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + Declining: "bg-red-500/10 text-red-600 dark:text-red-400", + New: "bg-blue-500/10 text-blue-600 dark:text-blue-400", + Dormant: "bg-foreground/[0.06] text-muted-foreground", + Flat: "bg-foreground/[0.06] text-muted-foreground", +}; + +function projectStatus(project: ProjectRow, windowDays: number): string { + const ageDays = (Date.now() - new Date(project.created_at).getTime()) / (24 * 60 * 60 * 1000); + if (ageDays <= windowDays) return "New"; + if (project.active_users === 0) return "Dormant"; + const g = growthPct(project.active_users, project.active_users_prev); + if (g == null) return "Growing"; + if (g > 10) return "Growing"; + if (g < -10) return "Declining"; + return "Flat"; +} + +type SortKey = "total_users" | "verified" | "active_users" | "signups" | "signup_growth" | "revenue"; + +function ProjectLeaderboard({ projects, windowDays }: { projects: ProjectRow[], windowDays: number }) { + const [sortKey, setSortKey] = useState("total_users"); + const [search, setSearch] = useState(""); + + const sorted = useMemo(() => { + const value = (p: ProjectRow): number => { + switch (sortKey) { + case "verified": { + return p.verified_users; + } + case "active_users": { + return p.active_users; + } + case "signups": { + return p.signups; + } + case "signup_growth": { + return growthPct(p.signups, p.signups_prev) ?? -Infinity; + } + case "revenue": { + return p.revenue_cents; + } + default: { + return p.total_users; + } + } + }; + const q = search.trim().toLowerCase(); + return projects + .filter((p) => q === "" || p.display_name.toLowerCase().includes(q) || p.id.toLowerCase().includes(q)) + .slice() + .sort((a, b) => value(b) - value(a)); + }, [projects, sortKey, search]); + + const header = (key: SortKey, label: string) => ( + + ); + + return ( + + +
+ Projects + setSearch(e.target.value)} + placeholder="Search projects" + className="w-48 rounded-lg border border-border/60 bg-transparent px-2.5 py-1 text-xs outline-none focus:border-foreground/30" + /> +
+
+
+
+ # + Project + {header("total_users", "Users")} + {header("verified", "Verified")} + {header("active_users", "Active")} + {header("signups", "Sign-ups")} + {header("signup_growth", "Growth")} + {header("revenue", "Revenue")} + Trend +
+
+ {sorted.map((project, index) => { + const status = projectStatus(project, windowDays); + const sg = growthPct(project.signups, project.signups_prev); + return ( +
+ {index + 1} +
+ {project.display_name || project.id} + {status} +
+ {formatCompact(project.total_users)} + {formatCompact(project.verified_users)} + {formatCompact(project.active_users)} + {formatCompact(project.signups)} + 0 ? "text-emerald-500 dark:text-emerald-400" : sg < 0 ? "text-red-500 dark:text-red-400" : "text-muted-foreground", + )}> + {sg == null ? "—" : `${sg > 0 ? "+" : ""}${sg}%`} + + {project.revenue_cents > 0 ? formatUsdFromCents(project.revenue_cents) : "—"} +
+
+ ); + })} + {sorted.length === 0 && ( + No projects match. + )} +
+
+
+
+
+ ); +} + +function Sparkline({ values }: { values: number[] }) { + const width = 56; + const height = 18; + if (values.length === 0) return ; + const max = Math.max(1, ...values); + const step = values.length > 1 ? width / (values.length - 1) : width; + const points = values.map((v, i) => `${(i * step).toFixed(1)},${(height - (v / max) * height).toFixed(1)}`).join(" "); + return ( + + + + ); +} + +const FEATURE_META = new Map([ + ["teams", { label: "Teams", icon: UsersThreeIcon }], + ["oauth", { label: "OAuth sign-in", icon: FingerprintSimpleIcon }], + ["emails", { label: "Emails", icon: EnvelopeSimpleIcon }], + ["analytics", { label: "Analytics SDK", icon: CursorClickIcon }], + ["payments", { label: "Payments", icon: CreditCardIcon }], + ["session_replay", { label: "Session replay", icon: MonitorPlayIcon }], +]); + +function FeatureAdoption({ features, totalProjects }: { features: Array<{ feature: string, projects_using: number }>, totalProjects: number }) { + const denominator = Math.max(1, totalProjects); + return ( + + + Feature adoption + {features.map((feature) => { + const meta = FEATURE_META.get(feature.feature); + const Icon = meta?.icon ?? ChartLineUpIcon; + const pctClamped = Math.max(0, Math.min(100, Math.round((feature.projects_using / denominator) * 100))); + return ( +
+
+ + + {meta?.label ?? feature.feature} + + {formatNumber(feature.projects_using)} ({pctClamped}%) +
+
+
+
+
+ ); + })} + + + ); +} + +function EmailHealth({ email }: { email: PlatformAnalytics["breakdowns"]["email"] }) { + const rows = [ + { label: "Sent", value: email.sent, color: "bg-blue-500" }, + { label: "Delivered", value: email.delivered, color: "bg-emerald-500" }, + { label: "Bounced", value: email.bounced, color: "bg-red-500" }, + { label: "Errored", value: email.error, color: "bg-amber-500" }, + { label: "In progress", value: email.in_progress, color: "bg-sky-400" }, + ]; + return ( + + + Email health + {rows.map((row) => ( +
+ + + {row.label} + + {formatNumber(row.value)} +
+ ))} +
+
+ ); +} + +function UxHealth({ deadClickRate }: { deadClickRate: number }) { + return ( + + + UX health + Dead clicks (clicks with no observable effect) +
+ 10 ? "text-red-500 dark:text-red-400" : deadClickRate > 4 ? "text-amber-500 dark:text-amber-400" : "text-emerald-500 dark:text-emerald-400", + )}> + {deadClickRate}% + +
+
+
+
+ + + ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page.tsx new file mode 100644 index 000000000..381fe96e8 --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page.tsx @@ -0,0 +1,9 @@ +import PageClient from "./page-client"; + +export const metadata = { + title: "Platform Analytics", +}; + +export default function Page() { + return ; +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx index cf521dd11..0667f4f31 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx @@ -28,6 +28,7 @@ import { CaretDownIcon, CaretRightIcon, ChartBarIcon, + ChartPieSliceIcon, CubeIcon, GearIcon, GlobeIcon, @@ -108,6 +109,16 @@ const dashboardsItem: Item = { type: 'item', }; +// Internal-only: platform-wide analytics across every project. Rendered solely +// when the active project is the internal (platform team) dashboard. +const platformAnalyticsItem: Item = { + name: "Platform Analytics", + href: "/platform-analytics", + regex: /^\/projects\/[^\/]+\/platform-analytics(\/.*)?$/, + icon: ChartPieSliceIcon, + type: 'item', +}; + const projectSettingsItem: AppSection = { name: "Project Settings", icon: GearIcon, @@ -528,6 +539,14 @@ function SidebarContent({ href={`/projects/${projectId}${dashboardsItem.href}`} isCollapsed={isCollapsed} /> + {projectId === "internal" && ( + + )}
diff --git a/apps/internal-tool/package.json b/apps/internal-tool/package.json index 13b2a3250..47f8b0f50 100644 --- a/apps/internal-tool/package.json +++ b/apps/internal-tool/package.json @@ -21,7 +21,7 @@ "@hexclave/shared": "workspace:*", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "next": "16.2.7", + "next": "16.2.9", "react-markdown": "^10.1.0", "zod": "^3.24.0", "remark-gfm": "^4.0.1", diff --git a/apps/mcp/package.json b/apps/mcp/package.json index a5f2b4a63..04f994ea7 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -15,7 +15,7 @@ "dependencies": { "@hexclave/shared": "workspace:*", "@vercel/mcp-adapter": "^1.0.0", - "next": "16.2.7", + "next": "16.2.9", "posthog-node": "^4.1.0", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/apps/skills/package.json b/apps/skills/package.json index f86c00181..1164718f9 100644 --- a/apps/skills/package.json +++ b/apps/skills/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@hexclave/shared": "workspace:*", - "next": "16.2.7", + "next": "16.2.9", "react": "19.2.3", "react-dom": "19.2.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0d3411c9..5c92dfbf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 1.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: ^10.45.0 - version: 10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) + version: 10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) '@simplewebauthn/server': specifier: ^13.3.0 version: 13.3.0 @@ -248,8 +248,8 @@ importers: specifier: ^1.0.6 version: 1.0.6 next: - specifier: 16.2.7 - version: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.2.9 + version: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nodemailer: specifier: ^6.9.10 version: 6.9.13 @@ -499,7 +499,7 @@ importers: version: 2.0.2(react@19.2.3) '@sentry/nextjs': specifier: ^10.11.0 - version: 10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) + version: 10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) '@stripe/connect-js': specifier: ^3.3.27 version: 3.3.27 @@ -520,10 +520,10 @@ importers: version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@vercel/analytics': specifier: ^1.2.2 - version: 1.3.1(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 1.3.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 1.0.12(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) ai: specifier: ^6.0.0 version: 6.0.81(zod@4.1.12) @@ -553,7 +553,7 @@ importers: version: 1.4.0 geist: specifier: ^1 - version: 1.3.0(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 1.3.0(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) input-otp: specifier: ^1.4.1 version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -573,11 +573,11 @@ importers: specifier: ^12.39.0 version: 12.39.0(@emotion/is-prop-valid@1.3.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: - specifier: 16.2.7 - version: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.2.9 + version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.2.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) posthog-js: specifier: ^1.336.1 version: 1.336.1 @@ -915,8 +915,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 next: - specifier: 16.2.7 - version: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.2.9 + version: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -968,10 +968,10 @@ importers: version: link:../../packages/shared '@vercel/mcp-adapter': specifier: ^1.0.0 - version: 1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) next: - specifier: 16.2.7 - version: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.2.9 + version: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) posthog-node: specifier: ^4.1.0 version: 4.1.0 @@ -1032,8 +1032,8 @@ importers: specifier: workspace:* version: link:../../packages/shared next: - specifier: 16.2.7 - version: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.2.9 + version: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -1212,7 +1212,7 @@ importers: devDependencies: mint: specifier: ^4.2.487 - version: 4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + version: 4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) examples/cjs-test: dependencies: @@ -1760,10 +1760,10 @@ importers: version: link:../../packages/next '@supabase/ssr': specifier: latest - version: 0.10.3(@supabase/supabase-js@2.108.0) + version: 0.12.0(@supabase/supabase-js@2.108.1) '@supabase/supabase-js': specifier: latest - version: 2.108.0 + version: 2.108.1 jose: specifier: ^5.2.2 version: 5.6.3 @@ -1993,7 +1993,7 @@ importers: devDependencies: '@quetzallabs/i18n': specifier: ^0.1.19 - version: 0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1)) + version: 0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1)) '@types/color': specifier: ^3.0.6 version: 3.0.6 @@ -2271,7 +2271,7 @@ importers: devDependencies: '@quetzallabs/i18n': specifier: ^0.1.19 - version: 0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) + version: 0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) '@types/color': specifier: ^3.0.6 version: 3.0.6 @@ -2426,7 +2426,7 @@ importers: devDependencies: '@sentry/nextjs': specifier: ^10.11.0 - version: 10.45.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) + version: 10.45.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) '@simplewebauthn/types': specifier: ^11.0.0 version: 11.0.0 @@ -2560,7 +2560,7 @@ importers: devDependencies: '@quetzallabs/i18n': specifier: ^0.1.19 - version: 0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@tanstack/react-router': specifier: ^1.167.4 version: 1.169.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -5759,8 +5759,8 @@ packages: '@next/env@15.5.19': resolution: {integrity: sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==} - '@next/env@16.2.7': - resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} + '@next/env@16.2.9': + resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==} '@next/eslint-plugin-next@14.2.17': resolution: {integrity: sha512-fW6/u1jjlBQrMs1ExyINehaK3B+LEW5UqdF6QYL07QK+SECkX0hnEyPMaNKj0ZFzirQ9D8jLWQ00P8oua4yx9g==} @@ -5786,8 +5786,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.2.7': - resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==} + '@next/swc-darwin-arm64@16.2.9': + resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -5804,8 +5804,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.2.7': - resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==} + '@next/swc-darwin-x64@16.2.9': + resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -5824,8 +5824,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-arm64-gnu@16.2.7': - resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} + '@next/swc-linux-arm64-gnu@16.2.9': + resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -5845,8 +5845,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-arm64-musl@16.2.7': - resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} + '@next/swc-linux-arm64-musl@16.2.9': + resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -5866,8 +5866,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-x64-gnu@16.2.7': - resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==} + '@next/swc-linux-x64-gnu@16.2.9': + resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -5887,8 +5887,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-x64-musl@16.2.7': - resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} + '@next/swc-linux-x64-musl@16.2.9': + resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -5906,8 +5906,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.2.7': - resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==} + '@next/swc-win32-arm64-msvc@16.2.9': + resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -5930,8 +5930,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.7': - resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==} + '@next/swc-win32-x64-msvc@16.2.9': + resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -9351,36 +9351,36 @@ packages: resolution: {integrity: sha512-SXuhqhuR5FXaYgKTXzZJeqtVA6JKb9IZWaGeEUxHHiOcFy2p51wccO72bYpXwoK4D5pzQOIYLTuAc7etxyMmwg==} engines: {node: '>=12.16'} - '@supabase/auth-js@2.108.0': - resolution: {integrity: sha512-0CzVGVqHfNOhRQVEcAmu58Mex2Ce0zL3aGfyV+iFQjTK6OntLK/hLCLr/VDRX0E8/2CdsiY99L7fZ/8ys/op4w==} + '@supabase/auth-js@2.108.1': + resolution: {integrity: sha512-Lle5rKU8f9LF3K5dDd8Or8mkkG+ptzRZZWKPVMm9B9UuovH65Ss2+iFnQqRsCqaGouvJEcTWyl0cj2riNrrDLQ==} engines: {node: '>=20.0.0'} - '@supabase/functions-js@2.108.0': - resolution: {integrity: sha512-lqEGDzT7QBUuKYzi5lHpV/XecXT9wikzcbXMbFo6krNpSDynD1sHM8wcsfB/BAqa4NkFuy3vF4JCV8MeakV8IQ==} + '@supabase/functions-js@2.108.1': + resolution: {integrity: sha512-fxBRW/A4IG7ADQztVt0NaEy5ysiO1WJ2pbldsnBchrkHuyepX0Krek9qA9T4gUQBVVTCE9Ea4pdsM5hfn3nc4A==} engines: {node: '>=20.0.0'} '@supabase/phoenix@0.4.2': resolution: {integrity: sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==} - '@supabase/postgrest-js@2.108.0': - resolution: {integrity: sha512-8AwTkPqowDYv/qh016CyXeZ3Ukpw6NHyfqc7DWV4afLR2hAiapf3zRKV2ZLG+//T1LK84HrR6X8VBwfgHWmNyw==} + '@supabase/postgrest-js@2.108.1': + resolution: {integrity: sha512-9lj2MCPPMgSTaJ5y+amnhb3TWPtMFVlbDn2hmX/VV91xQU4j0AauwfMaBErHBJ+zzsSwjc0jLU+zLIZFLQzfig==} engines: {node: '>=20.0.0'} - '@supabase/realtime-js@2.108.0': - resolution: {integrity: sha512-N3xR0u7TNr+c5wuLSU60rcfu/H/8N0WBs7iHWwjI/NxKwY3XWSyLUbpbpU8bzmL0dA/Gk9Mupri8mxKUXBW+iw==} + '@supabase/realtime-js@2.108.1': + resolution: {integrity: sha512-mHGGqOjwd1XTydcoffUqEMsbFQHUi6A3uhQ0EXr3iqzpLqItxKA9nbN6gIQxrZ7JRRnuUe/iOFPUkYV9Tdc5lg==} engines: {node: '>=20.0.0'} - '@supabase/ssr@0.10.3': - resolution: {integrity: sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==} + '@supabase/ssr@0.12.0': + resolution: {integrity: sha512-d9XV5XzJvzzZbeAIM7fWTCUYxQJZ2Ru6ny3dJHmHGp/LIrJ+o9FpD7N9Rf/UhhWEvHXSoDe8SI32Z2ouOdMjBg==} peerDependencies: - '@supabase/supabase-js': ^2.105.3 + '@supabase/supabase-js': ^2.108.0 - '@supabase/storage-js@2.108.0': - resolution: {integrity: sha512-zMYQmh87CId7d8i/1FIfv4fMDcXPutmIJSpoY58GLXi7M266MNzxWGNabDk4i555Oj1Nqtsu2i3Qo3rpWUXO6A==} + '@supabase/storage-js@2.108.1': + resolution: {integrity: sha512-Er0SGGt85iT6ye+SSh98Az6L2CesoZJuyzEZYH2oBOAnIxa9Nn4CtwUC3veGxYggoT56X+3tVuuQeDBP8kR8sg==} engines: {node: '>=20.0.0'} - '@supabase/supabase-js@2.108.0': - resolution: {integrity: sha512-AjPoimM9MZLZbddnlDBGmpZ/Tas1dNcJvuZy/VD1AfmrjBC8J2RSw6UOqR4ISLLlEioOLca/5t1crFnAxa0wRQ==} + '@supabase/supabase-js@2.108.1': + resolution: {integrity: sha512-V/1hRKLSCJ0zEL+9QFRBUtivvePfOsaAYQmC0HhFNSHC2F3xFs4jSF3YhkLmzex6E4V4FGvmBDOP72D/53NnZA==} engines: {node: '>=20.0.0'} '@swc/counter@0.1.3': @@ -15268,8 +15268,8 @@ packages: sass: optional: true - next@16.2.7: - resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==} + next@16.2.9: + resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -22174,13 +22174,13 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@mintlify/cli@4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/cli@4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: '@inquirer/prompts': 7.9.0(@types/node@20.17.6) - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/link-rot': 3.0.1010(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/previewing': 4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/link-rot': 3.0.1010(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/previewing': 4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/validation': 0.1.653(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) adm-zip: 0.5.16 chalk: 5.2.0 @@ -22280,7 +22280,7 @@ snapshots: - ts-node - typescript - '@mintlify/common@1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/common@1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: '@asyncapi/parser': 3.4.0 '@asyncapi/specs': 6.8.1 @@ -22322,7 +22322,7 @@ snapshots: remark-rehype: 11.1.1 remark-stringify: 11.0.0 sucrase: 3.35.0 - tailwindcss: 3.4.18(tsx@4.19.3)(yaml@2.6.0) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.0) unified: 11.0.5 unist-builder: 4.0.0 unist-util-map: 4.0.0 @@ -22344,7 +22344,7 @@ snapshots: - typescript - yaml - '@mintlify/common@1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/common@1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: '@asyncapi/parser': 3.4.0 '@asyncapi/specs': 6.8.1 @@ -22386,7 +22386,7 @@ snapshots: remark-rehype: 11.1.1 remark-stringify: 11.0.0 sucrase: 3.35.0 - tailwindcss: 3.4.18(tsx@4.19.3)(yaml@2.6.0) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.0) unified: 11.0.5 unist-builder: 4.0.0 unist-util-map: 4.0.0 @@ -22408,11 +22408,11 @@ snapshots: - typescript - yaml - '@mintlify/link-rot@3.0.1010(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/link-rot@3.0.1010(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/previewing': 4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/previewing': 4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/scraping': 4.0.522(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(typescript@5.9.3) '@mintlify/validation': 0.1.653(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(typescript@5.9.3) fs-extra: 11.1.0 @@ -22510,11 +22510,11 @@ snapshots: leven: 4.0.0 yaml: 2.8.0 - '@mintlify/prebuild@1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/prebuild@1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/openapi-parser': 0.0.8 - '@mintlify/scraping': 4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/scraping': 4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/validation': 0.1.653(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(typescript@5.9.3) chalk: 5.3.0 favicons: 7.2.0 @@ -22542,11 +22542,11 @@ snapshots: - utf-8-validate - yaml - '@mintlify/prebuild@1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/prebuild@1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/openapi-parser': 0.0.8 - '@mintlify/scraping': 4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/scraping': 4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/validation': 0.1.653(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) chalk: 5.3.0 favicons: 7.2.0 @@ -22574,10 +22574,10 @@ snapshots: - utf-8-validate - yaml - '@mintlify/previewing@4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/previewing@4.0.1038(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) - '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) + '@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/validation': 0.1.653(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) adm-zip: 0.5.16 better-opn: 3.0.2 @@ -22647,9 +22647,9 @@ snapshots: - typescript - utf-8-validate - '@mintlify/scraping@4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/scraping@4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@18.3.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/openapi-parser': 0.0.8 fs-extra: 11.1.1 hast-util-to-mdast: 10.1.0 @@ -22682,9 +22682,9 @@ snapshots: - utf-8-validate - yaml - '@mintlify/scraping@4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)': + '@mintlify/scraping@4.0.699(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: - '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) '@mintlify/openapi-parser': 0.0.8 fs-extra: 11.1.1 hast-util-to-mdast: 10.1.0 @@ -22834,7 +22834,7 @@ snapshots: '@next/env@15.5.19': {} - '@next/env@16.2.7': {} + '@next/env@16.2.9': {} '@next/eslint-plugin-next@14.2.17': dependencies: @@ -22858,7 +22858,7 @@ snapshots: '@next/swc-darwin-arm64@15.5.19': optional: true - '@next/swc-darwin-arm64@16.2.7': + '@next/swc-darwin-arm64@16.2.9': optional: true '@next/swc-darwin-x64@14.2.33': @@ -22867,7 +22867,7 @@ snapshots: '@next/swc-darwin-x64@15.5.19': optional: true - '@next/swc-darwin-x64@16.2.7': + '@next/swc-darwin-x64@16.2.9': optional: true '@next/swc-linux-arm64-gnu@14.2.33': @@ -22876,7 +22876,7 @@ snapshots: '@next/swc-linux-arm64-gnu@15.5.19': optional: true - '@next/swc-linux-arm64-gnu@16.2.7': + '@next/swc-linux-arm64-gnu@16.2.9': optional: true '@next/swc-linux-arm64-musl@14.2.33': @@ -22885,7 +22885,7 @@ snapshots: '@next/swc-linux-arm64-musl@15.5.19': optional: true - '@next/swc-linux-arm64-musl@16.2.7': + '@next/swc-linux-arm64-musl@16.2.9': optional: true '@next/swc-linux-x64-gnu@14.2.33': @@ -22894,7 +22894,7 @@ snapshots: '@next/swc-linux-x64-gnu@15.5.19': optional: true - '@next/swc-linux-x64-gnu@16.2.7': + '@next/swc-linux-x64-gnu@16.2.9': optional: true '@next/swc-linux-x64-musl@14.2.33': @@ -22903,7 +22903,7 @@ snapshots: '@next/swc-linux-x64-musl@15.5.19': optional: true - '@next/swc-linux-x64-musl@16.2.7': + '@next/swc-linux-x64-musl@16.2.9': optional: true '@next/swc-win32-arm64-msvc@14.2.33': @@ -22912,7 +22912,7 @@ snapshots: '@next/swc-win32-arm64-msvc@15.5.19': optional: true - '@next/swc-win32-arm64-msvc@16.2.7': + '@next/swc-win32-arm64-msvc@16.2.9': optional: true '@next/swc-win32-ia32-msvc@14.2.33': @@ -22924,7 +22924,7 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.19': optional: true - '@next/swc-win32-x64-msvc@16.2.7': + '@next/swc-win32-x64-msvc@16.2.9': optional: true '@node-oauth/formats@1.0.0': {} @@ -24438,7 +24438,7 @@ snapshots: - next - supports-color - '@quetzallabs/i18n@0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': + '@quetzallabs/i18n@0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': dependencies: '@babel/parser': 7.29.0 '@babel/traverse': 7.29.0 @@ -24446,7 +24446,7 @@ snapshots: dotenv: 10.0.0 i18next: 21.10.0 i18next-parser: 9.0.2 - next-intl: 3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@18.3.1) + next-intl: 3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@18.3.1) path: 0.12.7 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -24456,7 +24456,7 @@ snapshots: - next - supports-color - '@quetzallabs/i18n@0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))': + '@quetzallabs/i18n@0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))': dependencies: '@babel/parser': 7.29.0 '@babel/traverse': 7.29.0 @@ -24464,7 +24464,7 @@ snapshots: dotenv: 10.0.0 i18next: 21.10.0 i18next-parser: 9.0.2 - next-intl: 3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))(react@18.3.1) + next-intl: 3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))(react@18.3.1) path: 0.12.7 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -24474,7 +24474,7 @@ snapshots: - next - supports-color - '@quetzallabs/i18n@0.1.19(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@quetzallabs/i18n@0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: '@babel/parser': 7.29.0 '@babel/traverse': 7.29.0 @@ -24482,7 +24482,7 @@ snapshots: dotenv: 10.0.0 i18next: 21.10.0 i18next-parser: 9.0.2 - next-intl: 3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1) + next-intl: 3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1) path: 0.12.7 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -27090,7 +27090,7 @@ snapshots: '@sentry/core@10.45.0': {} - '@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': + '@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.37.0 @@ -27104,7 +27104,7 @@ snapshots: '@sentry/vercel-edge': 10.11.0 '@sentry/webpack-plugin': 4.3.0(webpack@5.92.0(esbuild@0.24.2)) chalk: 3.0.0 - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) resolve: 1.22.8 rollup: 4.50.1 stacktrace-parser: 0.1.11 @@ -27117,7 +27117,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': + '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 @@ -27130,7 +27130,7 @@ snapshots: '@sentry/react': 10.45.0(react@19.2.3) '@sentry/vercel-edge': 10.45.0 '@sentry/webpack-plugin': 5.1.1(webpack@5.92.0(esbuild@0.24.2)) - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rollup: 4.57.1 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -27142,7 +27142,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': + '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 @@ -27155,7 +27155,7 @@ snapshots: '@sentry/react': 10.45.0(react@19.2.3) '@sentry/vercel-edge': 10.45.0 '@sentry/webpack-plugin': 5.1.1(webpack@5.92.0(esbuild@0.24.2)) - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rollup: 4.57.1 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -28249,42 +28249,42 @@ snapshots: '@stripe/stripe-js@7.7.0': {} - '@supabase/auth-js@2.108.0': + '@supabase/auth-js@2.108.1': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.108.0': + '@supabase/functions-js@2.108.1': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.2': {} - '@supabase/postgrest-js@2.108.0': + '@supabase/postgrest-js@2.108.1': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.108.0': + '@supabase/realtime-js@2.108.1': dependencies: '@supabase/phoenix': 0.4.2 tslib: 2.8.1 - '@supabase/ssr@0.10.3(@supabase/supabase-js@2.108.0)': + '@supabase/ssr@0.12.0(@supabase/supabase-js@2.108.1)': dependencies: - '@supabase/supabase-js': 2.108.0 + '@supabase/supabase-js': 2.108.1 cookie: 1.0.2 - '@supabase/storage-js@2.108.0': + '@supabase/storage-js@2.108.1': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.108.0': + '@supabase/supabase-js@2.108.1': dependencies: - '@supabase/auth-js': 2.108.0 - '@supabase/functions-js': 2.108.0 - '@supabase/postgrest-js': 2.108.0 - '@supabase/realtime-js': 2.108.0 - '@supabase/storage-js': 2.108.0 + '@supabase/auth-js': 2.108.1 + '@supabase/functions-js': 2.108.1 + '@supabase/postgrest-js': 2.108.1 + '@supabase/realtime-js': 2.108.1 + '@supabase/storage-js': 2.108.1 '@swc/counter@0.1.3': {} @@ -29807,23 +29807,23 @@ snapshots: jose: 5.6.3 neverthrow: 7.2.0 - '@vercel/analytics@1.3.1(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@vercel/analytics@1.3.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 '@vercel/functions@2.0.0(@aws-sdk/credential-provider-web-identity@3.972.27)': optionalDependencies: '@aws-sdk/credential-provider-web-identity': 3.972.27 - '@vercel/mcp-adapter@1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@vercel/mcp-adapter@1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: '@modelcontextprotocol/sdk': 1.17.2 - mcp-handler: 1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + mcp-handler: 1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) optionalDependencies: - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@vercel/oidc@3.1.0': {} @@ -29849,9 +29849,9 @@ snapshots: xdg-app-paths: 5.1.0 zod: 3.24.4 - '@vercel/speed-insights@1.0.12(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@vercel/speed-insights@1.0.12(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': optionalDependencies: - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 '@vitejs/plugin-react@4.3.3(vite@7.3.1(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.19.3)(yaml@2.6.0))': @@ -33691,9 +33691,9 @@ snapshots: transitivePeerDependencies: - supports-color - geist@1.3.0(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + geist@1.3.0(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) generate-function@2.3.1: dependencies: @@ -35364,14 +35364,14 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + mcp-handler@1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: '@modelcontextprotocol/sdk': 1.17.2 chalk: 5.6.2 commander: 11.1.0 redis: 4.7.1 optionalDependencies: - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) mdast-util-find-and-replace@3.0.2: dependencies: @@ -36018,9 +36018,9 @@ snapshots: dependencies: minipass: 7.1.3 - mint@4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0): + mint@4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0): dependencies: - '@mintlify/cli': 4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0) + '@mintlify/cli': 4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.0) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -36155,27 +36155,27 @@ snapshots: react: 18.3.1 use-intl: 3.19.1(react@18.3.1) - next-intl@3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@18.3.1): + next-intl@3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@18.3.1): dependencies: '@formatjs/intl-localematcher': 0.5.4 negotiator: 0.6.4 - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 18.3.1 use-intl: 3.19.1(react@18.3.1) - next-intl@3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))(react@18.3.1): + next-intl@3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1))(react@18.3.1): dependencies: '@formatjs/intl-localematcher': 0.5.4 negotiator: 0.6.4 - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1) react: 18.3.1 use-intl: 3.19.1(react@18.3.1) - next-intl@3.19.1(next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1): + next-intl@3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1): dependencies: '@formatjs/intl-localematcher': 0.5.4 negotiator: 0.6.4 - next: 16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 18.3.1 use-intl: 3.19.1(react@18.3.1) @@ -36217,9 +36217,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-themes@0.2.1(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-themes@0.2.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -36388,9 +36388,9 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.2.7 + '@next/env': 16.2.9 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.16 caniuse-lite: 1.0.30001751 @@ -36399,23 +36399,23 @@ snapshots: react-dom: 19.2.1(react@19.2.1) styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.7 - '@next/swc-darwin-x64': 16.2.7 - '@next/swc-linux-arm64-gnu': 16.2.7 - '@next/swc-linux-arm64-musl': 16.2.7 - '@next/swc-linux-x64-gnu': 16.2.7 - '@next/swc-linux-x64-musl': 16.2.7 - '@next/swc-win32-arm64-msvc': 16.2.7 - '@next/swc-win32-x64-msvc': 16.2.7 + '@next/swc-darwin-arm64': 16.2.9 + '@next/swc-darwin-x64': 16.2.9 + '@next/swc-linux-arm64-gnu': 16.2.9 + '@next/swc-linux-arm64-musl': 16.2.9 + '@next/swc-linux-x64-gnu': 16.2.9 + '@next/swc-linux-x64-musl': 16.2.9 + '@next/swc-win32-arm64-msvc': 16.2.9 + '@next/swc-win32-x64-msvc': 16.2.9 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1): + next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.2.7 + '@next/env': 16.2.9 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.16 caniuse-lite: 1.0.30001751 @@ -36424,23 +36424,23 @@ snapshots: react-dom: 19.2.3(react@19.2.1) styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.7 - '@next/swc-darwin-x64': 16.2.7 - '@next/swc-linux-arm64-gnu': 16.2.7 - '@next/swc-linux-arm64-musl': 16.2.7 - '@next/swc-linux-x64-gnu': 16.2.7 - '@next/swc-linux-x64-musl': 16.2.7 - '@next/swc-win32-arm64-msvc': 16.2.7 - '@next/swc-win32-x64-msvc': 16.2.7 + '@next/swc-darwin-arm64': 16.2.9 + '@next/swc-darwin-x64': 16.2.9 + '@next/swc-linux-arm64-gnu': 16.2.9 + '@next/swc-linux-arm64-musl': 16.2.9 + '@next/swc-linux-x64-gnu': 16.2.9 + '@next/swc-linux-x64-musl': 16.2.9 + '@next/swc-win32-arm64-msvc': 16.2.9 + '@next/swc-win32-x64-msvc': 16.2.9 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@16.2.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 16.2.7 + '@next/env': 16.2.9 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.16 caniuse-lite: 1.0.30001751 @@ -36449,23 +36449,23 @@ snapshots: react-dom: 19.2.3(react@19.2.3) styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.7 - '@next/swc-darwin-x64': 16.2.7 - '@next/swc-linux-arm64-gnu': 16.2.7 - '@next/swc-linux-arm64-musl': 16.2.7 - '@next/swc-linux-x64-gnu': 16.2.7 - '@next/swc-linux-x64-musl': 16.2.7 - '@next/swc-win32-arm64-msvc': 16.2.7 - '@next/swc-win32-x64-msvc': 16.2.7 + '@next/swc-darwin-arm64': 16.2.9 + '@next/swc-darwin-x64': 16.2.9 + '@next/swc-linux-arm64-gnu': 16.2.9 + '@next/swc-linux-arm64-musl': 16.2.9 + '@next/swc-linux-x64-gnu': 16.2.9 + '@next/swc-linux-x64-musl': 16.2.9 + '@next/swc-win32-arm64-msvc': 16.2.9 + '@next/swc-win32-x64-msvc': 16.2.9 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 16.2.7 + '@next/env': 16.2.9 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.16 caniuse-lite: 1.0.30001751 @@ -36474,14 +36474,14 @@ snapshots: react-dom: 19.2.3(react@19.2.3) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.7 - '@next/swc-darwin-x64': 16.2.7 - '@next/swc-linux-arm64-gnu': 16.2.7 - '@next/swc-linux-arm64-musl': 16.2.7 - '@next/swc-linux-x64-gnu': 16.2.7 - '@next/swc-linux-x64-musl': 16.2.7 - '@next/swc-win32-arm64-msvc': 16.2.7 - '@next/swc-win32-x64-msvc': 16.2.7 + '@next/swc-darwin-arm64': 16.2.9 + '@next/swc-darwin-x64': 16.2.9 + '@next/swc-linux-arm64-gnu': 16.2.9 + '@next/swc-linux-arm64-musl': 16.2.9 + '@next/swc-linux-x64-gnu': 16.2.9 + '@next/swc-linux-x64-musl': 16.2.9 + '@next/swc-win32-arm64-msvc': 16.2.9 + '@next/swc-win32-x64-msvc': 16.2.9 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: @@ -37196,15 +37196,6 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.6.0): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.6 - tsx: 4.19.3 - yaml: 2.6.0 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 @@ -39532,34 +39523,6 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.18(tsx@4.19.3)(yaml@2.6.0): - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.6.0) - postcss-nested: 6.2.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - resolve: 1.22.11 - sucrase: 3.35.0 - transitivePeerDependencies: - - tsx - - yaml - tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.0): dependencies: '@alloc/quick-lru': 5.2.0