diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 4fc3033..5460b73 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -33,7 +33,13 @@ "vitest": "catalog:" }, "dependencies": { + "@lucia-auth/adapter-postgresql": "^3.1.2", + "@node-rs/argon2": "^2.0.2", "@rocicorp/zero": "^0.24.3000000000", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "layerchart": "2.0.0-next.43", + "lucia": "^3.2.2", "postgres": "catalog:", "zod": "catalog:" } diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css index 08bd70a..8856233 100644 --- a/apps/dashboard/src/app.css +++ b/apps/dashboard/src/app.css @@ -21,3 +21,25 @@ html, body { background-color: var(--color-rp-base); } + +/* LayerChart axis styling */ +.lc-axis text { + fill: var(--color-rp-subtle); + stroke: none; + font-size: 12px; +} + +.lc-axis line, +.lc-axis path { + stroke: var(--color-rp-overlay); +} + +/* LayerChart tooltip styling */ +.lc-tooltip { + background-color: var(--color-rp-surface); + border: 1px solid var(--color-rp-overlay); + border-radius: 4px; + padding: 8px; + color: var(--color-rp-text); + font-size: 12px; +} diff --git a/apps/dashboard/src/app.d.ts b/apps/dashboard/src/app.d.ts new file mode 100644 index 0000000..cd6e124 --- /dev/null +++ b/apps/dashboard/src/app.d.ts @@ -0,0 +1,12 @@ +import type { Session, User } from "lucia"; + +declare global { + namespace App { + interface Locals { + user: User | null; + session: Session | null; + } + } +} + +export {}; diff --git a/apps/dashboard/src/app.html b/apps/dashboard/src/app.html index 22bff6b..a1e98f4 100644 --- a/apps/dashboard/src/app.html +++ b/apps/dashboard/src/app.html @@ -2,7 +2,7 @@ - + %sveltekit.head% diff --git a/apps/dashboard/src/hooks.server.ts b/apps/dashboard/src/hooks.server.ts new file mode 100644 index 0000000..cb5477d --- /dev/null +++ b/apps/dashboard/src/hooks.server.ts @@ -0,0 +1,34 @@ +import { lucia } from "$lib/server/auth"; +import type { Handle } from "@sveltejs/kit"; + +export const handle: Handle = async ({ event, resolve }) => { + const sessionId = event.cookies.get(lucia.sessionCookieName); + + if (!sessionId) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const { session, user } = await lucia.validateSession(sessionId); + + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + } + + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + } + + event.locals.user = user; + event.locals.session = session; + return resolve(event); +}; diff --git a/apps/dashboard/src/lib/components/EventsList.svelte b/apps/dashboard/src/lib/components/EventsList.svelte index dc5c63a..1bcc9a6 100644 --- a/apps/dashboard/src/lib/components/EventsList.svelte +++ b/apps/dashboard/src/lib/components/EventsList.svelte @@ -6,7 +6,8 @@ let { projectId }: { projectId: string } = $props(); // Fetch a larger set and paginate client-side - const eventsQuery = useQuery(recentEvents(projectId, 500)); + // Use getter function so query re-runs if projectId changes + const eventsQuery = useQuery(() => recentEvents(projectId, 500)); // Client-side pagination let displayLimit = $state(100); diff --git a/apps/dashboard/src/lib/components/FlagsList.svelte b/apps/dashboard/src/lib/components/FlagsList.svelte index e49dea6..baaa3e2 100644 --- a/apps/dashboard/src/lib/components/FlagsList.svelte +++ b/apps/dashboard/src/lib/components/FlagsList.svelte @@ -6,7 +6,8 @@ let { projectId }: { projectId: string } = $props(); const zero = getZero(); - const flagsQuery = useQuery(flagsForProject(projectId)); + // Use getter function so query re-runs if projectId changes + const flagsQuery = useQuery(() => flagsForProject(projectId)); // Form state let newFlagKey = $state(""); diff --git a/apps/dashboard/src/lib/components/OverviewDashboard.svelte b/apps/dashboard/src/lib/components/OverviewDashboard.svelte index 9cdb892..696697a 100644 --- a/apps/dashboard/src/lib/components/OverviewDashboard.svelte +++ b/apps/dashboard/src/lib/components/OverviewDashboard.svelte @@ -7,33 +7,60 @@ recentEvents, } from "$lib/zero/queries"; import type { Event, Session, Flag } from "$lib/zero/schema"; + import EventsOverTimeChart from "./charts/EventsOverTimeChart.svelte"; + import SessionsTrendChart from "./charts/SessionsTrendChart.svelte"; + import TopEventsChart from "./charts/TopEventsChart.svelte"; // Props - projectId is guaranteed to be defined when this component mounts let { projectId }: { projectId: string } = $props(); - // Use synced queries - const eventsQuery = useQuery(eventsInWindow(projectId, 7)); - const sessionsQuery = useQuery(sessionsInWindow(projectId, 7)); - const flagsQuery = useQuery(enabledFlags(projectId)); - const recentEventsQuery = useQuery(recentEvents(projectId, 10)); + // Date range state + let days = $state(7); + + // Reactive synced queries - use getter functions so queries re-run when params change + const eventsQuery = useQuery(() => eventsInWindow(projectId, days)); + const sessionsQuery = useQuery(() => sessionsInWindow(projectId, days)); + const flagsQuery = useQuery(() => enabledFlags(projectId)); + const recentQuery = useQuery(() => recentEvents(projectId, 10)); // Computed values + const loading = $derived( + eventsQuery.loading || sessionsQuery.loading || flagsQuery.loading || recentQuery.loading + ); const eventCount = $derived(eventsQuery.data.length); const sessionCount = $derived(sessionsQuery.data.length); const flagCount = $derived(flagsQuery.data.length); - const recentEventsList = $derived(recentEventsQuery.data); - - const loading = $derived( - eventsQuery.loading || - sessionsQuery.loading || - flagsQuery.loading || - recentEventsQuery.loading - ); + +
+

Analytics Overview

+
+ + + +
+
+ +
-
Events (7d)
+
Events ({days}d)
{#if loading} ... @@ -44,7 +71,7 @@
-
Sessions (7d)
+
Sessions ({days}d)
{#if loading} ... @@ -66,6 +93,38 @@
+ +
+
+

Events Over Time

+ {#if loading} +
Loading...
+ {:else} + + {/if} +
+ +
+

Sessions Over Time

+ {#if loading} +
Loading...
+ {:else} + + {/if} +
+
+ + +
+

Top Events

+ {#if loading} +
Loading...
+ {:else} + + {/if} +
+ +

Recent Events

@@ -73,7 +132,7 @@
{#if loading}

Loading events...

- {:else if recentEventsList.length === 0} + {:else if recentQuery.data.length === 0}

No events yet. Integrate the SDK to start tracking.

{:else} @@ -85,7 +144,7 @@ - {#each recentEventsList as event} + {#each recentQuery.data as event}
{event.event_name} diff --git a/apps/dashboard/src/lib/components/SessionDetail.svelte b/apps/dashboard/src/lib/components/SessionDetail.svelte index 927c9e9..7c279b4 100644 --- a/apps/dashboard/src/lib/components/SessionDetail.svelte +++ b/apps/dashboard/src/lib/components/SessionDetail.svelte @@ -5,8 +5,9 @@ let { sessionId }: { sessionId: string } = $props(); - const sessionQuery = useQueryOne(sessionById(sessionId)); - const eventsQuery = useQuery(eventsForSession(sessionId)); + // Use getter functions so queries re-run if sessionId changes + const sessionQuery = useQueryOne(() => sessionById(sessionId)); + const eventsQuery = useQuery(() => eventsForSession(sessionId)); const loading = $derived(sessionQuery.loading || eventsQuery.loading); const session = $derived(sessionQuery.data); diff --git a/apps/dashboard/src/lib/components/SessionsList.svelte b/apps/dashboard/src/lib/components/SessionsList.svelte index eaedb64..a6ad40c 100644 --- a/apps/dashboard/src/lib/components/SessionsList.svelte +++ b/apps/dashboard/src/lib/components/SessionsList.svelte @@ -5,7 +5,8 @@ let { projectId }: { projectId: string } = $props(); - const sessionsQuery = useQuery(recentSessions(projectId, 100)); + // Use getter function so query re-runs if projectId changes + const sessionsQuery = useQuery(() => recentSessions(projectId, 100)); function formatTimestamp(ts: number): string { return new Date(ts).toLocaleString(); diff --git a/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte b/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte new file mode 100644 index 0000000..b5989ac --- /dev/null +++ b/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte @@ -0,0 +1,99 @@ + + +
+ {#if hasData} + + + + { + const date = d as Date; + return `${date.getMonth() + 1}/${date.getDate()}`; + }} + /> + + + + + + {#if data} + + {data.date.toLocaleDateString()} + + + + + {/if} + + + {:else} +
+ No data available +
+ {/if} +
diff --git a/apps/dashboard/src/lib/components/charts/SessionsTrendChart.svelte b/apps/dashboard/src/lib/components/charts/SessionsTrendChart.svelte new file mode 100644 index 0000000..4b2db30 --- /dev/null +++ b/apps/dashboard/src/lib/components/charts/SessionsTrendChart.svelte @@ -0,0 +1,94 @@ + + +
+ {#if hasData} + + + + { + const date = d as Date; + return `${date.getMonth() + 1}/${date.getDate()}`; + }} + /> + + + + + {#if data} + + {data.date.toLocaleDateString()} + + + + + {/if} + + + {:else} +
+ No data available +
+ {/if} +
diff --git a/apps/dashboard/src/lib/components/charts/TopEventsChart.svelte b/apps/dashboard/src/lib/components/charts/TopEventsChart.svelte new file mode 100644 index 0000000..c40c2c8 --- /dev/null +++ b/apps/dashboard/src/lib/components/charts/TopEventsChart.svelte @@ -0,0 +1,63 @@ + + +
+ {#if hasData} + + + + + + + + + {#if data} + {data.name} + + + + {/if} + + + {:else} +
+ No events recorded +
+ {/if} +
diff --git a/apps/dashboard/src/lib/server/auth.ts b/apps/dashboard/src/lib/server/auth.ts new file mode 100644 index 0000000..8136adc --- /dev/null +++ b/apps/dashboard/src/lib/server/auth.ts @@ -0,0 +1,33 @@ +import { Lucia } from "lucia"; +import { PostgresJsAdapter } from "@lucia-auth/adapter-postgresql"; +import { sql } from "$lib/db"; +import { dev } from "$app/environment"; + +const adapter = new PostgresJsAdapter(sql, { + user: "dashboard_users", + session: "dashboard_sessions", +}); + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !dev, + }, + }, + getUserAttributes: (attributes) => { + return { + email: attributes.email, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + email: string; +} diff --git a/apps/dashboard/src/lib/zero/client.svelte.ts b/apps/dashboard/src/lib/zero/client.svelte.ts index 8d1faf3..3d79fb9 100644 --- a/apps/dashboard/src/lib/zero/client.svelte.ts +++ b/apps/dashboard/src/lib/zero/client.svelte.ts @@ -5,6 +5,25 @@ * Handles subscription lifecycle and cleanup automatically. * * Uses Svelte context for proper component tree integration. + * + * ## Reactive Parameters + * + * Query hooks support both static queries and getter functions for reactive params. + * Use getter functions when query parameters depend on reactive state: + * + * ```typescript + * // Static - query params never change + * const events = useQuery(recentEvents('project-123', 100)); + * + * // Reactive - re-runs when `days` state changes + * let days = $state(7); + * const events = useQuery(() => eventsInWindow(projectId, days)); + * ``` + * + * Why getter functions? Svelte 5's `$effect` only tracks reactive values that are + * **synchronously read** during execution. By passing a getter, the function is + * called inside the effect, allowing Svelte to track dependencies like `days`. + * Without this, the query would capture `days = 7` at mount time and never update. */ import { schema, type Schema } from "./schema"; @@ -23,6 +42,7 @@ let _zeroInstance: ZeroClient | null = null; /** * Initialize Zero client and set it in Svelte context. * Call this once in your root layout (client-side only). + * Idempotent - returns existing instance if already created. * * @param userID - Unique identifier for the current user (use 'dashboard-admin' for admin) * @param server - Zero cache server URL (default: http://localhost:4848) @@ -32,6 +52,11 @@ export function createZero( userID: string = "dashboard-admin", server: string = "http://localhost:4848", ): ZeroClient { + // Return existing instance if already initialized (idempotent) + if (_zeroInstance) { + return _zeroInstance; + } + const zero = new Zero({ server, schema, @@ -71,11 +96,19 @@ export interface QueryResult { readonly loading: boolean; } +/** + * Query input type - either a query object or a getter function for reactive params. + */ +type QueryInput = + | Query + | (() => Query); + /** * Create a reactive query that automatically updates when data changes. * Handles subscription lifecycle and cleanup automatically. * - * Works with synced queries - just pass the result of calling your query function. + * Works with synced queries. Pass either a query directly or a getter function + * for reactive parameters: * * @example * ```svelte @@ -83,7 +116,11 @@ export interface QueryResult { * import { useQuery } from '$lib/zero/client.svelte'; * import { recentEvents } from '$lib/zero/queries'; * - * const events = useQuery(recentEvents(projectId, 100)); + * // Static query (no reactive params): + * const events = useQuery(recentEvents('project-123', 100)); + * + * // Reactive query (re-runs when projectId or limit changes): + * const events = useQuery(() => recentEvents(projectId, limit)); * * * {#each events.data as event} @@ -94,13 +131,18 @@ export interface QueryResult { export function useQuery< TTable extends keyof Schema["tables"] & string, TReturn, ->(query: Query): QueryResult { +>(queryInput: QueryInput): QueryResult { const zero = getZero(); let data = $state([]); let loading = $state(true); $effect(() => { + // Resolve query - call getter if function, otherwise use directly + // This makes reactive dependencies work when using getter form + const query = + typeof queryInput === "function" ? queryInput() : queryInput; + // Materialize the query (returns a View) const view = zero.materialize(query); @@ -158,7 +200,11 @@ export interface QueryOneResult { * import { useQueryOne } from '$lib/zero/client.svelte'; * import { projectById } from '$lib/zero/queries'; * - * const project = useQueryOne(projectById(projectId)); + * // Static query: + * const project = useQueryOne(projectById('project-123')); + * + * // Reactive query (re-runs when projectId changes): + * const project = useQueryOne(() => projectById(projectId)); * * * {#if project.data} @@ -169,13 +215,17 @@ export interface QueryOneResult { export function useQueryOne< TTable extends keyof Schema["tables"] & string, TReturn, ->(query: Query): QueryOneResult { +>(queryInput: QueryInput): QueryOneResult { const zero = getZero(); let data = $state(undefined); let loading = $state(true); $effect(() => { + // Resolve query - call getter if function, otherwise use directly + const query = + typeof queryInput === "function" ? queryInput() : queryInput; + const view = zero.materialize(query); const unsubscribe = view.addListener((result) => { diff --git a/apps/dashboard/src/routes/+layout.server.ts b/apps/dashboard/src/routes/+layout.server.ts new file mode 100644 index 0000000..6e02226 --- /dev/null +++ b/apps/dashboard/src/routes/+layout.server.ts @@ -0,0 +1,17 @@ +import { redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ locals, url }) => { + const publicPaths = ["/login", "/register"]; + const isPublicPath = publicPaths.some((path) => + url.pathname.startsWith(path), + ); + + if (!locals.user && !isPublicPath) { + redirect(302, "/login"); + } + + return { + user: locals.user, + }; +}; diff --git a/apps/dashboard/src/routes/+layout.svelte b/apps/dashboard/src/routes/+layout.svelte index eb63ca1..063369e 100644 --- a/apps/dashboard/src/routes/+layout.svelte +++ b/apps/dashboard/src/routes/+layout.svelte @@ -2,25 +2,71 @@ import "../app.css"; import { PUBLIC_ZERO_SERVER } from "$env/static/public"; import { createZero } from "$lib/zero/client.svelte"; + import { enhance } from "$app/forms"; + import { untrack } from "svelte"; + import type { LayoutData } from "./$types"; + import type { Snippet } from "svelte"; - let { children } = $props(); + let { children, data }: { children: Snippet; data: LayoutData } = $props(); - // Initialize Zero client (SSR disabled in +layout.ts) - createZero("dashboard-admin", PUBLIC_ZERO_SERVER || "http://localhost:4848"); + // Derive user reactively for template usage + const user = $derived(data.user); + + // Track Zero initialization state - gates children rendering + let zeroReady = $state(false); + + // Initialize Zero synchronously during script execution for direct page loads + // Using untrack because this intentionally captures the initial value + untrack(() => { + if (data.user) { + createZero(data.user.id, PUBLIC_ZERO_SERVER || "http://localhost:4848"); + zeroReady = true; + } + }); + + // Handle user changes after initial mount (e.g., login redirect) + // Effect runs after initial render, but children are gated by zeroReady + $effect(() => { + if (data.user && !zeroReady) { + createZero(data.user.id, PUBLIC_ZERO_SERVER || "http://localhost:4848"); + zeroReady = true; + } + }); -
- +{#if user} + {#if zeroReady} +
+ -
- {@render children()} -
-
+
+ {@render children()} +
+
+ {:else} +
+ Initializing... +
+ {/if} +{:else} + {@render children()} +{/if} diff --git a/apps/dashboard/src/routes/api/zero/get-queries/+server.ts b/apps/dashboard/src/routes/api/zero/get-queries/+server.ts index 23a03dd..aef7e79 100644 --- a/apps/dashboard/src/routes/api/zero/get-queries/+server.ts +++ b/apps/dashboard/src/routes/api/zero/get-queries/+server.ts @@ -17,10 +17,11 @@ const validatedQueries = Object.fromEntries( queries.map((q) => [q.queryName, withValidation(q)]), ); -export const POST: RequestHandler = async ({ request }) => { - // For now, no auth - pass null context - // When auth is added, extract from locals.user - const authContext: AuthContext = null; +export const POST: RequestHandler = async ({ request, locals }) => { + // Extract user from session (set by hooks.server.ts) + const authContext: AuthContext = locals.user + ? { userId: locals.user.id } + : null; // Query resolver function with auth context in closure function getQuery(name: string, args: readonly unknown[]) { diff --git a/apps/dashboard/src/routes/login/+layout.svelte b/apps/dashboard/src/routes/login/+layout.svelte new file mode 100644 index 0000000..122e0d0 --- /dev/null +++ b/apps/dashboard/src/routes/login/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/apps/dashboard/src/routes/login/+page.server.ts b/apps/dashboard/src/routes/login/+page.server.ts new file mode 100644 index 0000000..762b07e --- /dev/null +++ b/apps/dashboard/src/routes/login/+page.server.ts @@ -0,0 +1,46 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { verify } from "@node-rs/argon2"; +import { lucia } from "$lib/server/auth"; +import { sql } from "$lib/db"; +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, "/"); + } + return {}; +}; + +export const actions: Actions = { + default: async ({ request, cookies }) => { + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + + if (typeof email !== "string" || typeof password !== "string") { + return fail(400, { error: "Invalid input" }); + } + + const [user] = await sql` + SELECT id, email, password_hash FROM dashboard_users WHERE email = ${email} + `; + + if (!user) { + return fail(400, { error: "Invalid email or password" }); + } + + const validPassword = await verify(user.password_hash, password); + if (!validPassword) { + return fail(400, { error: "Invalid email or password" }); + } + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + + redirect(302, "/"); + }, +}; diff --git a/apps/dashboard/src/routes/login/+page.svelte b/apps/dashboard/src/routes/login/+page.svelte new file mode 100644 index 0000000..62b4c4e --- /dev/null +++ b/apps/dashboard/src/routes/login/+page.svelte @@ -0,0 +1,57 @@ + + +
+
+

Beacon

+

Sign in to your account

+ + {#if form?.error} +
+ {form.error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ + +
+ +

+ Don't have an account? + Register +

+
+
diff --git a/apps/dashboard/src/routes/logout/+page.server.ts b/apps/dashboard/src/routes/logout/+page.server.ts new file mode 100644 index 0000000..51991ff --- /dev/null +++ b/apps/dashboard/src/routes/logout/+page.server.ts @@ -0,0 +1,24 @@ +import { redirect } from "@sveltejs/kit"; +import { lucia } from "$lib/server/auth"; +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async () => { + redirect(302, "/"); +}; + +export const actions: Actions = { + default: async ({ locals, cookies }) => { + if (!locals.session) { + redirect(302, "/login"); + } + + await lucia.invalidateSession(locals.session.id); + const sessionCookie = lucia.createBlankSessionCookie(); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + + redirect(302, "/login"); + }, +}; diff --git a/apps/dashboard/src/routes/register/+layout.svelte b/apps/dashboard/src/routes/register/+layout.svelte new file mode 100644 index 0000000..122e0d0 --- /dev/null +++ b/apps/dashboard/src/routes/register/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/apps/dashboard/src/routes/register/+page.server.ts b/apps/dashboard/src/routes/register/+page.server.ts new file mode 100644 index 0000000..4139b16 --- /dev/null +++ b/apps/dashboard/src/routes/register/+page.server.ts @@ -0,0 +1,61 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { hash } from "@node-rs/argon2"; +import { lucia } from "$lib/server/auth"; +import { sql } from "$lib/db"; +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, "/"); + } + return {}; +}; + +export const actions: Actions = { + default: async ({ request, cookies }) => { + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + + if (typeof email !== "string" || typeof password !== "string") { + return fail(400, { error: "Invalid input" }); + } + + if (password.length < 8) { + return fail(400, { error: "Password must be at least 8 characters" }); + } + + // Check if email already exists + const [existing] = await sql` + SELECT id FROM dashboard_users WHERE email = ${email} + `; + + if (existing) { + return fail(400, { error: "Email already registered" }); + } + + // Hash password and create user + const passwordHash = await hash(password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + const [user] = await sql` + INSERT INTO dashboard_users (email, password_hash) + VALUES (${email}, ${passwordHash}) + RETURNING id + `; + + // Create session + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + + redirect(302, "/"); + }, +}; diff --git a/apps/dashboard/src/routes/register/+page.svelte b/apps/dashboard/src/routes/register/+page.svelte new file mode 100644 index 0000000..ce7f013 --- /dev/null +++ b/apps/dashboard/src/routes/register/+page.svelte @@ -0,0 +1,59 @@ + + +
+
+

Beacon

+

Create an account

+ + {#if form?.error} +
+ {form.error} +
+ {/if} + +
+
+ + +
+ +
+ + +

Minimum 8 characters

+
+ + +
+ +

+ Already have an account? + Sign in +

+
+
diff --git a/apps/dashboard/static/favicon.svg b/apps/dashboard/static/favicon.svg new file mode 100644 index 0000000..48f1a37 --- /dev/null +++ b/apps/dashboard/static/favicon.svg @@ -0,0 +1,4 @@ + + + B + diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/apps/dashboard/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/docs/quickstart.md b/docs/quickstart.md index 6294c17..0d11e62 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -94,7 +94,7 @@ function MyApp() { Open http://localhost:5173 to see tracked events and manage feature flags. -> **WIP**: Dashboard authentication is not yet implemented. Currently open access for local development. +> **Note**: You'll need to register an account on first visit. Go to `/register` to create credentials. --- diff --git a/docs/roadmap.md b/docs/roadmap.md index 5f05be9..cc1f216 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -15,12 +15,12 @@ Beacon's path to a stable alpha release. - [x] Documentation (SDK, API, protocol, identity) - [x] SDK unit tests (95% coverage) -### 🚧 M1: Examples & Polish +### ✅ M1: Examples & Polish - [x] React example app - [x] Flag management UI -- [ ] Dashboard authentication -- [ ] Basic analytics visualizations +- [x] Dashboard authentication (Lucia + session cookies) +- [x] Basic analytics visualizations (LayerChart) ### ✅ M2: Benchmarking diff --git a/infra/migrations/002_dashboard_auth.sql b/infra/migrations/002_dashboard_auth.sql new file mode 100644 index 0000000..565596f --- /dev/null +++ b/infra/migrations/002_dashboard_auth.sql @@ -0,0 +1,24 @@ +-- migrate:up + +-- Dashboard users (separate from analytics users table) +CREATE TABLE dashboard_users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Dashboard sessions (for Lucia auth) +CREATE TABLE dashboard_sessions ( + id TEXT PRIMARY KEY, + user_id UUID NOT NULL REFERENCES dashboard_users(id) ON DELETE CASCADE, + expires_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX idx_dashboard_sessions_user ON dashboard_sessions(user_id); + + +-- migrate:down + +DROP TABLE IF EXISTS dashboard_sessions; +DROP TABLE IF EXISTS dashboard_users; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f53533..b45bab7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,9 +119,27 @@ importers: apps/dashboard: dependencies: + '@lucia-auth/adapter-postgresql': + specifier: ^3.1.2 + version: 3.1.2(lucia@3.2.2)(postgres@3.4.7) + '@node-rs/argon2': + specifier: ^2.0.2 + version: 2.0.2 '@rocicorp/zero': specifier: ^0.24.3000000000 version: 0.24.3000000000(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)) + d3-scale: + specifier: ^4.0.2 + version: 4.0.2 + d3-shape: + specifier: ^3.2.0 + version: 3.2.0 + layerchart: + specifier: 2.0.0-next.43 + version: 2.0.0-next.43(svelte@5.45.3) + lucia: + specifier: ^3.2.2 + version: 3.2.2 postgres: specifier: 'catalog:' version: 3.4.7 @@ -398,6 +416,13 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dagrejs/dagre@1.1.8': + resolution: {integrity: sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==} + + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} + '@databases/escape-identifier@1.0.3': resolution: {integrity: sha512-Su36iSVzaHxpVdISVMViUX/32sLvzxVgjZpYhzhotxZUuLo11GVWsiHwqkvUZijTLUxcDmUqEwGJO3O/soLuZA==} @@ -420,6 +445,15 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -794,6 +828,15 @@ packages: '@fastify/websocket@11.2.0': resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@google-cloud/precise-date@4.0.0': resolution: {integrity: sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==} engines: {node: '>=14.0.0'} @@ -842,6 +885,37 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@layerstack/svelte-actions@1.0.1-next.14': + resolution: {integrity: sha512-MPBmVaB+GfNHvBkg5nJkPG18smoXKvsvJRpsdWnrUBfca+TieZLoaEzNxDH+9LG11dIXP9gghsXt1mUqbbyAsA==} + + '@layerstack/svelte-state@0.1.0-next.19': + resolution: {integrity: sha512-yCYoQAIbeP8y1xmOB/r0+UundgP4JFnpNURgMki+26TotzoqrZ5oLpHvhPSVm60ks+buR3ebDBTeUFdHzxwzQQ==} + + '@layerstack/tailwind@2.0.0-next.17': + resolution: {integrity: sha512-ZSn6ouqpnzB6DKzSKLVwrUBOQsrzpDA/By2/ba9ApxgTGnaD1nyqNwrvmZ+kswdAwB4YnrGEAE4VZkKrB2+DaQ==} + + '@layerstack/utils@2.0.0-next.14': + resolution: {integrity: sha512-1I2CS0Cwgs53W35qVg1eBdYhB/CiPvL3s0XE61b8jWkTHxgjBF65yYNgXjW74kv7WI7GsJcWMNBufPd0rnu9kA==} + + '@lucia-auth/adapter-postgresql@3.1.2': + resolution: {integrity: sha512-XgScy312JsaiyJZ0OaUHakk01hFBldF1m8abX4Ctk2Dkt7lEVS/u0xZ2Sf6Hlji1wZtEM0uiIWdeMUu79XcXZA==} + deprecated: This package has been deprecated. Please see https://lucia-auth.com/lucia-v3/migrate. + peerDependencies: + '@neondatabase/serverless': 0.7 - 0.9 + lucia: 3.x + pg: ^8.8.0 + postgres: ^3.3.0 + peerDependenciesMeta: + '@neondatabase/serverless': + optional: true + pg: + optional: true + postgres: + optional: true + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -854,6 +928,93 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@node-rs/argon2-android-arm-eabi@2.0.2': + resolution: {integrity: sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/argon2-android-arm64@2.0.2': + resolution: {integrity: sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/argon2-darwin-arm64@2.0.2': + resolution: {integrity: sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/argon2-darwin-x64@2.0.2': + resolution: {integrity: sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/argon2-freebsd-x64@2.0.2': + resolution: {integrity: sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/argon2-linux-arm-gnueabihf@2.0.2': + resolution: {integrity: sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/argon2-linux-arm64-gnu@2.0.2': + resolution: {integrity: sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-arm64-musl@2.0.2': + resolution: {integrity: sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-x64-gnu@2.0.2': + resolution: {integrity: sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-linux-x64-musl@2.0.2': + resolution: {integrity: sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-wasm32-wasi@2.0.2': + resolution: {integrity: sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/argon2-win32-arm64-msvc@2.0.2': + resolution: {integrity: sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/argon2-win32-ia32-msvc@2.0.2': + resolution: {integrity: sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/argon2-win32-x64-msvc@2.0.2': + resolution: {integrity: sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/argon2@2.0.2': + resolution: {integrity: sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==} + engines: {node: '>= 10'} + '@opentelemetry/api-logs@0.203.0': resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} @@ -1339,6 +1500,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + + '@oslojs/crypto@1.0.1': + resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1688,6 +1861,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aws-lambda@8.10.152': resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} @@ -2089,6 +2265,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -2136,6 +2316,109 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo-voronoi@2.1.0: + resolution: {integrity: sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate-path@2.3.0: + resolution: {integrity: sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ==} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-tile@1.0.0: + resolution: {integrity: sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q==} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-tricontour@1.1.0: + resolution: {integrity: sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==} + engines: {node: '>=12'} + data-urls@6.0.0: resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} engines: {node: '>=20'} @@ -2174,6 +2457,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2569,6 +2855,13 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2730,6 +3023,11 @@ packages: known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + layerchart@2.0.0-next.43: + resolution: {integrity: sha512-1Ywm38NdzHWKwgaAHq3EcqshIgsq+pylntSnVWAVazXUk/NsxPcxdpR3tMt3ySjWV0ZPBBgLs78sdVf7FTgd+g==} + peerDependencies: + svelte: ^5.0.0 + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2829,6 +3127,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -2845,6 +3146,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucia@3.2.2: + resolution: {integrity: sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA==} + deprecated: This package has been deprecated. Please see https://lucia-auth.com/lucia-v3/migrate. + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2862,6 +3167,10 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + memoize@10.2.0: + resolution: {integrity: sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2869,6 +3178,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -3225,11 +3538,22 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + runed@0.31.1: + resolution: {integrity: sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==} + peerDependencies: + svelte: ^5.7.0 + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -3395,6 +3719,9 @@ packages: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + tailwindcss@4.1.17: resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} @@ -3472,6 +3799,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -3909,6 +4239,12 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dagrejs/dagre@1.1.8': + dependencies: + '@dagrejs/graphlib': 2.2.4 + + '@dagrejs/graphlib@2.2.4': {} + '@databases/escape-identifier@1.0.3': dependencies: '@databases/validate-unicode': 1.0.0 @@ -3935,6 +4271,22 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -4174,6 +4526,17 @@ snapshots: - bufferutil - utf-8-validate + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + '@google-cloud/precise-date@4.0.0': {} '@grpc/grpc-js@1.14.1': @@ -4220,6 +4583,44 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@layerstack/svelte-actions@1.0.1-next.14': + dependencies: + '@floating-ui/dom': 1.7.4 + '@layerstack/utils': 2.0.0-next.14 + d3-scale: 4.0.2 + + '@layerstack/svelte-state@0.1.0-next.19': + dependencies: + '@layerstack/utils': 2.0.0-next.14 + + '@layerstack/tailwind@2.0.0-next.17': + dependencies: + '@layerstack/utils': 2.0.0-next.14 + clsx: 2.1.1 + d3-array: 3.2.4 + lodash-es: 4.17.21 + tailwind-merge: 3.4.0 + + '@layerstack/utils@2.0.0-next.14': + dependencies: + d3-array: 3.2.4 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + lodash-es: 4.17.21 + + '@lucia-auth/adapter-postgresql@3.1.2(lucia@3.2.2)(postgres@3.4.7)': + dependencies: + lucia: 3.2.2 + optionalDependencies: + postgres: 3.4.7 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@noble/ciphers@1.3.0': {} '@noble/curves@1.9.7': @@ -4228,6 +4629,67 @@ snapshots: '@noble/hashes@1.8.0': {} + '@node-rs/argon2-android-arm-eabi@2.0.2': + optional: true + + '@node-rs/argon2-android-arm64@2.0.2': + optional: true + + '@node-rs/argon2-darwin-arm64@2.0.2': + optional: true + + '@node-rs/argon2-darwin-x64@2.0.2': + optional: true + + '@node-rs/argon2-freebsd-x64@2.0.2': + optional: true + + '@node-rs/argon2-linux-arm-gnueabihf@2.0.2': + optional: true + + '@node-rs/argon2-linux-arm64-gnu@2.0.2': + optional: true + + '@node-rs/argon2-linux-arm64-musl@2.0.2': + optional: true + + '@node-rs/argon2-linux-x64-gnu@2.0.2': + optional: true + + '@node-rs/argon2-linux-x64-musl@2.0.2': + optional: true + + '@node-rs/argon2-wasm32-wasi@2.0.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@node-rs/argon2-win32-arm64-msvc@2.0.2': + optional: true + + '@node-rs/argon2-win32-ia32-msvc@2.0.2': + optional: true + + '@node-rs/argon2-win32-x64-msvc@2.0.2': + optional: true + + '@node-rs/argon2@2.0.2': + optionalDependencies: + '@node-rs/argon2-android-arm-eabi': 2.0.2 + '@node-rs/argon2-android-arm64': 2.0.2 + '@node-rs/argon2-darwin-arm64': 2.0.2 + '@node-rs/argon2-darwin-x64': 2.0.2 + '@node-rs/argon2-freebsd-x64': 2.0.2 + '@node-rs/argon2-linux-arm-gnueabihf': 2.0.2 + '@node-rs/argon2-linux-arm64-gnu': 2.0.2 + '@node-rs/argon2-linux-arm64-musl': 2.0.2 + '@node-rs/argon2-linux-x64-gnu': 2.0.2 + '@node-rs/argon2-linux-x64-musl': 2.0.2 + '@node-rs/argon2-wasm32-wasi': 2.0.2 + '@node-rs/argon2-win32-arm64-msvc': 2.0.2 + '@node-rs/argon2-win32-ia32-msvc': 2.0.2 + '@node-rs/argon2-win32-x64-msvc': 2.0.2 + '@opentelemetry/api-logs@0.203.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -4930,6 +5392,19 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + + '@oslojs/binary@1.0.0': {} + + '@oslojs/crypto@1.0.1': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + + '@oslojs/encoding@1.1.0': {} + '@pinojs/redact@0.4.0': {} '@polka/url@1.0.0-next.29': {} @@ -5258,6 +5733,11 @@ snapshots: tailwindcss: 4.1.17 vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aws-lambda@8.10.152': {} '@types/babel__core@7.20.5': @@ -5719,6 +6199,8 @@ snapshots: commander@4.1.1: {} + commander@7.2.0: {} + commondir@1.0.1: {} compare-utf8@0.1.1: {} @@ -5756,6 +6238,106 @@ snapshots: csstype@3.2.3: {} + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo-voronoi@2.1.0: + dependencies: + d3-array: 3.2.4 + d3-delaunay: 6.0.4 + d3-geo: 3.1.1 + d3-tricontour: 1.1.0 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate-path@2.3.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-tile@1.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-tricontour@1.1.0: + dependencies: + d3-delaunay: 6.0.4 + d3-scale: 4.0.2 + data-urls@6.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -5785,6 +6367,10 @@ snapshots: defu@6.1.4: {} + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -6261,6 +6847,10 @@ snapshots: ini@1.3.8: {} + internmap@1.0.1: {} + + internmap@2.0.3: {} + ipaddr.js@2.3.0: {} is-arguments@1.2.0: @@ -6417,6 +7007,37 @@ snapshots: known-css-properties@0.37.0: {} + layerchart@2.0.0-next.43(svelte@5.45.3): + dependencies: + '@dagrejs/dagre': 1.1.8 + '@layerstack/svelte-actions': 1.0.1-next.14 + '@layerstack/svelte-state': 0.1.0-next.19 + '@layerstack/tailwind': 2.0.0-next.17 + '@layerstack/utils': 2.0.0-next.14 + d3-array: 3.2.4 + d3-color: 3.1.0 + d3-delaunay: 6.0.4 + d3-dsv: 3.0.1 + d3-force: 3.0.0 + d3-geo: 3.1.1 + d3-geo-voronoi: 2.1.0 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-interpolate-path: 2.3.0 + d3-path: 3.1.0 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-sankey: 0.12.3 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-shape: 3.2.0 + d3-tile: 1.0.0 + d3-time: 3.1.0 + lodash-es: 4.17.21 + memoize: 10.2.0 + runed: 0.31.1(svelte@5.45.3) + svelte: 5.45.3 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -6491,6 +7112,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} lodash.merge@4.6.2: {} @@ -6503,6 +7126,11 @@ snapshots: dependencies: yallist: 3.1.1 + lucia@3.2.2: + dependencies: + '@oslojs/crypto': 1.0.1 + '@oslojs/encoding': 1.1.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6521,10 +7149,16 @@ snapshots: mdn-data@2.12.2: {} + memoize@10.2.0: + dependencies: + mimic-function: 5.0.1 + merge-stream@2.0.0: {} mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: {} minimatch@3.1.2: @@ -6840,6 +7474,8 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.2: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -6868,6 +7504,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + runed@0.31.1(svelte@5.45.3): + dependencies: + esm-env: 1.2.2 + svelte: 5.45.3 + + rw@1.3.3: {} + sade@1.8.1: dependencies: mri: 1.2.0 @@ -7039,6 +7682,8 @@ snapshots: array-back: 6.2.2 wordwrapjs: 5.1.1 + tailwind-merge@3.4.0: {} + tailwindcss@4.1.17: {} tapable@2.3.0: {} @@ -7109,6 +7754,9 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: + optional: true + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.0)