mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-27 21:01:03 +08:00
add platform analytics route to the dashboard (#1626)
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md
-->
<!-- This is an auto-generated description by cubic. -->
---
## 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.
<sup>Written for commit 3c803a8915.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1626?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
This commit is contained in:
parent
3493df464b
commit
25b0414d59
@ -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",
|
||||
|
||||
@ -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<string, number> {
|
||||
const out = new Map<string, number>();
|
||||
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<string, unknown>)[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<string, unknown>).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 <T,>(query: string, params: Record<string, unknown>): Promise<T[]> => {
|
||||
const result = await clickhouse.query({ query, query_params: params, format: "JSONEachRow" });
|
||||
return await result.json<T>();
|
||||
};
|
||||
|
||||
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<CountRow>(`
|
||||
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<CountRow>(`
|
||||
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<CountRow>(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.teams FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams),
|
||||
chQuery<CountRow>(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.connected_accounts FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams),
|
||||
chQuery<CountRow>(`SELECT project_id AS projectId, count() AS c FROM analytics_internal.email_outboxes FINAL WHERE ${customerUserScope} GROUP BY project_id`, baseParams),
|
||||
chQuery<CountRow>(`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<Array<{ day: string, cents: string | number }>>(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<Array<{ projectId: string, cur: string | number, prev: string | number }>>(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<Array<{ projectId: string, product: unknown, priceId: string | null, quantity: number }>>(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<Array<{ method: string, count: number }>>(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<Array<{ sent: number, delivered: number, bounced: number, error: number, in_progress: number, deliveredCur: number, finishedCur: number, deliveredPrev: number, finishedPrev: number }>>(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<Array<{ projectId: string }>>(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<Array<{ projectId: string }>>(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<string, number>();
|
||||
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<string, number>) => {
|
||||
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<string>) => {
|
||||
const seen = new Set<string>();
|
||||
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<string, Map<string, number>>();
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
54
apps/backend/src/lib/platform-admin.test.ts
Normal file
54
apps/backend/src/lib/platform-admin.test.ts
Normal file
@ -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<typeof isPlatformAdmin>[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.]`
|
||||
);
|
||||
});
|
||||
});
|
||||
25
apps/backend/src/lib/platform-admin.ts
Normal file
25
apps/backend/src/lib/platform-admin.ts
Normal file
@ -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<boolean> {
|
||||
const managedProjectIds = await listManagedProjectIds(user);
|
||||
return managedProjectIds.includes("internal");
|
||||
}
|
||||
|
||||
export async function ensurePlatformAdmin(user: UsersCrud["Admin"]["Read"]): Promise<void> {
|
||||
if (!(await isPlatformAdmin(user))) {
|
||||
throw new StatusError(403, "You do not have access to platform analytics.");
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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<string, number>,
|
||||
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<Response>,
|
||||
};
|
||||
|
||||
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 (
|
||||
<PageLayout
|
||||
title="Platform Analytics"
|
||||
description="Platform-wide usage across every project. Visible only to the internal team."
|
||||
>
|
||||
<PlatformAnalyticsContent />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformAnalyticsContent() {
|
||||
const app = useStackApp();
|
||||
const appInternals = useMemo(() => getStackAppInternals(app), [app]);
|
||||
const [state, setState] = useState<LoadState>({ 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 (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
|
||||
{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-24 w-full rounded-xl" />)}
|
||||
</div>
|
||||
<Skeleton className="h-80 w-full rounded-xl" />
|
||||
<Skeleton className="h-96 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "forbidden") {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-2 py-12 text-center">
|
||||
<LockKeyIcon className="h-6 w-6 text-muted-foreground" />
|
||||
<Typography type="h3">Access restricted</Typography>
|
||||
<Typography variant="secondary" className="max-w-md text-sm">
|
||||
Platform analytics is limited to members of the internal project's team.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "error") {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-10 text-center">
|
||||
<Typography variant="secondary">Could not load platform analytics. Please try again.</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return <Dashboard data={state.data} range={range} onRangeChange={setRange} />;
|
||||
}
|
||||
|
||||
function Dashboard({
|
||||
data,
|
||||
range,
|
||||
onRangeChange,
|
||||
}: {
|
||||
data: PlatformAnalytics,
|
||||
range: 7 | 30,
|
||||
onRangeChange: (range: 7 | 30) => void,
|
||||
}) {
|
||||
const slice = <T,>(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<string, StackedDataPoint>();
|
||||
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 (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-end">
|
||||
<RangeToggle range={range} onRangeChange={onRangeChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
{tiles.map((tile) => (
|
||||
<KpiTile key={tile.label} label={tile.label} kpi={tile.kpi} format={tile.format} suffixDelta={tile.suffixDelta} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<ChartCard title="Platform growth" subtitle="Active users, visitors, page views and revenue" className="xl:col-span-2" gradient="blue">
|
||||
{composed.length === 0
|
||||
? <EmptyChart />
|
||||
: <ComposedAnalyticsChart datapoints={composed} showVisitors showPageViews showRevenue height={300} />}
|
||||
</ChartCard>
|
||||
<ChartCard title="Growth quality" subtitle="New / retained / reactivated users" gradient="green">
|
||||
{stacked.length === 0
|
||||
? <EmptyChart />
|
||||
: <StackedBarChartDisplay datapoints={stacked} height={300} />}
|
||||
</ChartCard>
|
||||
</div>
|
||||
|
||||
<ProjectLeaderboard projects={data.projects} windowDays={data.window_days} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<ChartCard title="Where users are" subtitle="Active users by country" gradient="cyan">
|
||||
<div className="h-[320px] w-full">
|
||||
<GlobeSection countryData={data.breakdowns.users_by_country} totalUsers={k.total_users.value} interactive />
|
||||
</div>
|
||||
</ChartCard>
|
||||
<ChartCard title="Sign-in methods" subtitle="How end users authenticate" gradient="purple">
|
||||
{data.breakdowns.auth_methods.length === 0
|
||||
? <EmptyChart />
|
||||
: <DonutChartDisplay datapoints={data.breakdowns.auth_methods} gradientColor="purple" />}
|
||||
</ChartCard>
|
||||
<ChartCard title="User mix" subtitle="Verified, unverified and anonymous" gradient="orange">
|
||||
<DonutChartDisplay
|
||||
datapoints={[
|
||||
{ method: "Verified", count: data.breakdowns.users_by_status.verified },
|
||||
{ method: "Unverified", count: data.breakdowns.users_by_status.unverified },
|
||||
{ method: "Anonymous", count: data.breakdowns.users_by_status.anonymous },
|
||||
]}
|
||||
gradientColor="orange"
|
||||
/>
|
||||
</ChartCard>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<FeatureAdoption features={data.feature_adoption} totalProjects={data.total_projects} />
|
||||
<EmailHealth email={data.breakdowns.email} />
|
||||
<UxHealth deadClickRate={data.breakdowns.dead_click_rate} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RangeToggle({ range, onRangeChange }: { range: 7 | 30, onRangeChange: (range: 7 | 30) => void }) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 rounded-xl bg-foreground/[0.06] p-1">
|
||||
{([7, 30] as const).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => onRangeChange(value)}
|
||||
className={cn(
|
||||
"rounded-lg px-3 py-1 text-xs font-medium transition-colors hover:transition-none",
|
||||
range === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{value}D
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-1 py-4">
|
||||
<Typography variant="secondary" className="truncate text-[11px] uppercase tracking-wide">{label}</Typography>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-semibold tabular-nums text-foreground">{format(kpi.value)}</span>
|
||||
{delta != null && delta !== 0 && (
|
||||
<span className={cn(
|
||||
"text-xs font-semibold tabular-nums",
|
||||
delta > 0 ? "text-emerald-500 dark:text-emerald-400" : "text-red-500 dark:text-red-400",
|
||||
)}>
|
||||
{delta > 0 ? "+" : ""}{delta}{suffixDelta === "pp" ? "pp" : "%"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartCard({
|
||||
title,
|
||||
subtitle,
|
||||
className,
|
||||
gradient,
|
||||
children,
|
||||
}: {
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
className?: string,
|
||||
gradient: "blue" | "cyan" | "purple" | "green" | "orange" | "slate",
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<DesignAnalyticsCard gradient={gradient} className={cn("flex flex-col", className)}>
|
||||
<div className="flex flex-col gap-0.5 px-5 pt-4">
|
||||
<Typography className="text-sm font-semibold text-foreground">{title}</Typography>
|
||||
{subtitle && <Typography variant="secondary" className="text-xs">{subtitle}</Typography>}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 px-3 pb-3 pt-2">{children}</div>
|
||||
</DesignAnalyticsCard>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyChart() {
|
||||
return (
|
||||
<div className="flex h-[280px] items-center justify-center">
|
||||
<Typography variant="secondary" className="text-xs">No data for this period.</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, string> = {
|
||||
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<SortKey>("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) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSortKey(key)}
|
||||
className={cn("text-right tabular-nums transition-colors hover:text-foreground", sortKey === key ? "text-foreground" : "")}
|
||||
>
|
||||
{label}{sortKey === key ? " ↓" : ""}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-3 py-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Typography className="text-sm font-semibold">Projects</Typography>
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-[820px]">
|
||||
<div className="grid grid-cols-[1.5rem_minmax(10rem,1.5fr)_5rem_4.5rem_5rem_5rem_5rem_5rem_4rem] items-center gap-3 border-b border-border/60 pb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
<span>#</span>
|
||||
<span>Project</span>
|
||||
{header("total_users", "Users")}
|
||||
{header("verified", "Verified")}
|
||||
{header("active_users", "Active")}
|
||||
{header("signups", "Sign-ups")}
|
||||
{header("signup_growth", "Growth")}
|
||||
{header("revenue", "Revenue")}
|
||||
<span className="text-right">Trend</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border/40">
|
||||
{sorted.map((project, index) => {
|
||||
const status = projectStatus(project, windowDays);
|
||||
const sg = growthPct(project.signups, project.signups_prev);
|
||||
return (
|
||||
<div
|
||||
key={project.id}
|
||||
className="grid grid-cols-[1.5rem_minmax(10rem,1.5fr)_5rem_4.5rem_5rem_5rem_5rem_5rem_4rem] items-center gap-3 py-2.5 text-sm"
|
||||
>
|
||||
<span className="tabular-nums text-muted-foreground">{index + 1}</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground">{project.display_name || project.id}</span>
|
||||
<span className={cn("shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold", STATUS_STYLES[status])}>{status}</span>
|
||||
</div>
|
||||
<span className="text-right tabular-nums text-foreground">{formatCompact(project.total_users)}</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{formatCompact(project.verified_users)}</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{formatCompact(project.active_users)}</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{formatCompact(project.signups)}</span>
|
||||
<span className={cn(
|
||||
"text-right text-xs tabular-nums",
|
||||
sg == null ? "text-muted-foreground" : sg > 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}%`}
|
||||
</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{project.revenue_cents > 0 ? formatUsdFromCents(project.revenue_cents) : "—"}</span>
|
||||
<div className="flex justify-end"><Sparkline values={project.sparkline} /></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sorted.length === 0 && (
|
||||
<Typography variant="secondary" className="py-6 text-center text-sm">No projects match.</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Sparkline({ values }: { values: number[] }) {
|
||||
const width = 56;
|
||||
const height = 18;
|
||||
if (values.length === 0) return <span className="text-muted-foreground">—</span>;
|
||||
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 (
|
||||
<svg width={width} height={height} className="overflow-visible text-foreground/40" aria-hidden>
|
||||
<polyline points={points} fill="none" stroke="currentColor" strokeWidth={1.25} strokeLinejoin="round" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const FEATURE_META = new Map<string, { label: string, icon: React.ElementType }>([
|
||||
["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 (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-3 py-5">
|
||||
<Typography className="text-sm font-semibold">Feature adoption</Typography>
|
||||
{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 (
|
||||
<div key={feature.feature} className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-2 text-sm">
|
||||
<span className="flex items-center gap-2 text-foreground">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" weight="regular" />
|
||||
{meta?.label ?? feature.feature}
|
||||
</span>
|
||||
<span className="tabular-nums text-muted-foreground">{formatNumber(feature.projects_using)} <span className="text-xs">({pctClamped}%)</span></span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-foreground/[0.06]">
|
||||
<div className="h-full rounded-full bg-foreground/30" style={{ width: `${pctClamped}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-3 py-5">
|
||||
<Typography className="text-sm font-semibold">Email health</Typography>
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="flex items-center justify-between gap-2 text-sm">
|
||||
<span className="flex items-center gap-2 text-foreground">
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full", row.color)} />
|
||||
{row.label}
|
||||
</span>
|
||||
<span className="tabular-nums text-muted-foreground">{formatNumber(row.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function UxHealth({ deadClickRate }: { deadClickRate: number }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-2 py-5">
|
||||
<Typography className="text-sm font-semibold">UX health</Typography>
|
||||
<Typography variant="secondary" className="text-xs">Dead clicks (clicks with no observable effect)</Typography>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={cn(
|
||||
"text-3xl font-semibold tabular-nums",
|
||||
deadClickRate > 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}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-foreground/[0.06]">
|
||||
<div className="h-full rounded-full bg-foreground/30" style={{ width: `${Math.min(100, deadClickRate)}%` }} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Platform Analytics",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <PageClient />;
|
||||
}
|
||||
@ -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" && (
|
||||
<NavItem
|
||||
item={platformAnalyticsItem}
|
||||
onClick={onNavigate}
|
||||
href={`/projects/${projectId}${platformAnalyticsItem.href}`}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn("mt-6 mb-3 transition-opacity duration-200", isCollapsed ? "opacity-0 h-0 mt-2 mb-0 overflow-hidden" : "opacity-100")}>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
397
pnpm-lock.yaml
397
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user