diff --git a/.env.template b/.env.template
index 83e39beb..ea20fd71 100644
--- a/.env.template
+++ b/.env.template
@@ -16,8 +16,16 @@ DB_USER=test
DB_PASSWORD=test
DB_NAME=scriptio
-# Staging Basic Auth (htpasswd format, escape $ as $$ in this file)
+# Basic Auth (htpasswd format, escape $ as $$ in this file)
+PROD_AUTH_USERS=
STAGING_AUTH_USERS=
+MONITORING_AUTH_USERS=
+
+# Monitoring
+GRAFANA_ADMIN_PASSWORD=
+METRICS_BEARER_TOKEN=
+APP_MEMORY_LIMIT_BYTES=536870912 # 512 MiB; tune to the prod container's memory budget
+DB_SIZE_LIMIT_BYTES=10737418240 # 10 GiB; tune to the prod DB's expected ceiling
# S3 storage
S3_BUCKET=
diff --git a/Dockerfile b/Dockerfile
index 3fbba538..0db35525 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,5 +26,5 @@ EXPOSE 3000
USER node
ENV NEXT_TELEMETRY_DISABLED 1
-ENTRYPOINT ["./launch.sh"]
+ENTRYPOINT ["./scripts/launch.sh"]
CMD [ "npm", "start" ]
\ No newline at end of file
diff --git a/components/admin/AdminShell.module.css b/components/admin/AdminShell.module.css
new file mode 100644
index 00000000..07024496
--- /dev/null
+++ b/components/admin/AdminShell.module.css
@@ -0,0 +1,133 @@
+.wrapper {
+ display: flex;
+ width: 100%;
+ height: 100vh;
+ background: var(--main-bg);
+}
+
+.sidebar {
+ width: 240px;
+ flex-shrink: 0;
+ background: var(--secondary);
+ border-right: 1px solid var(--separator);
+ display: flex;
+ flex-direction: column;
+ padding: 24px 16px;
+ gap: 24px;
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 8px;
+ color: var(--primary-text);
+ font-family: var(--font-josefin), sans-serif;
+ font-weight: 700;
+ font-size: 18px;
+ letter-spacing: 0.02em;
+}
+
+.brandBadge {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: var(--tertiary);
+ color: var(--primary-text);
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ font-family: var(--font-inter), sans-serif;
+ font-weight: 600;
+}
+
+.nav {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.navGroupLabel {
+ font-family: var(--font-inter), sans-serif;
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--secondary-text);
+ padding: 12px 12px 4px;
+}
+
+.navItem {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ background: transparent;
+ border: none;
+ border-radius: 8px;
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ cursor: pointer;
+ text-align: left;
+ text-decoration: none;
+ transition: background-color 120ms ease;
+}
+
+.navItem:hover {
+ background: var(--primary-hover);
+}
+
+.navItemActive {
+ background: var(--tertiary);
+}
+
+.navItemActive:hover {
+ background: var(--tertiary-hover);
+}
+
+.footer {
+ margin-top: auto;
+ padding: 12px;
+ border-top: 1px solid var(--separator);
+ color: var(--secondary-text);
+ font-size: 12px;
+ font-family: var(--font-inter), sans-serif;
+}
+
+.footerEmail {
+ color: var(--primary-text);
+ font-weight: 600;
+ display: block;
+ margin-bottom: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.main {
+ flex: 1;
+ overflow-y: auto;
+ padding: 32px 40px;
+ min-width: 0;
+}
+
+.header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+
+.title {
+ color: var(--primary-text);
+ font-family: var(--font-josefin), sans-serif;
+ font-size: 28px;
+ font-weight: 700;
+}
+
+.subtitle {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+}
diff --git a/components/admin/AdminShell.tsx b/components/admin/AdminShell.tsx
new file mode 100644
index 00000000..a0db4ec0
--- /dev/null
+++ b/components/admin/AdminShell.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { ReactNode } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { BarChart3, Users, FolderOpen, LogOut } from "lucide-react";
+import styles from "./AdminShell.module.css";
+
+type NavLink = { href: string; label: string; icon: ReactNode };
+
+const NAV_LINKS: NavLink[] = [
+ { href: "/admin", label: "Overview", icon: },
+ { href: "/admin/users", label: "Users", icon: },
+ { href: "/admin/projects", label: "Projects", icon: },
+];
+
+type Props = {
+ email: string;
+ title: string;
+ subtitle?: string;
+ children: ReactNode;
+};
+
+export default function AdminShell({ email, title, subtitle, children }: Props) {
+ const pathname = usePathname() ?? "";
+
+ return (
+
+
+
+
+
{title}
+ {subtitle && {subtitle}}
+
+ {children}
+
+
+ );
+}
diff --git a/components/admin/ProjectDetail.module.css b/components/admin/ProjectDetail.module.css
new file mode 100644
index 00000000..eace3676
--- /dev/null
+++ b/components/admin/ProjectDetail.module.css
@@ -0,0 +1,137 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.backLink {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--secondary-text);
+ text-decoration: none;
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ margin-bottom: 8px;
+ width: fit-content;
+}
+
+.backLink:hover {
+ color: var(--primary-text);
+}
+
+.card {
+ background: var(--secondary);
+ border: 1px solid var(--separator);
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: var(--panel-shadow);
+}
+
+.cardTitle {
+ color: var(--primary-text);
+ font-family: var(--font-josefin), sans-serif;
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 16px;
+}
+
+.fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px 24px;
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.fieldLabel {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.fieldValue {
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ overflow-wrap: anywhere;
+}
+
+.idValue {
+ font-family: var(--font-courier), monospace;
+ font-size: 13px;
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--separator);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.tableRow {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 16px;
+ padding: 12px 16px;
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ color: var(--primary-text);
+ border-bottom: 1px solid var(--separator);
+ align-items: center;
+}
+
+.tableRow:last-child {
+ border-bottom: none;
+}
+
+.tableHeader {
+ background: var(--primary);
+ color: var(--secondary-text);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ font-weight: 600;
+}
+
+.tableRowLink {
+ text-decoration: none;
+ transition: background-color 120ms ease;
+ cursor: pointer;
+}
+
+.tableRowLink:hover {
+ background: var(--primary-hover);
+}
+
+.tableCellMuted {
+ color: var(--secondary-text);
+ font-size: 12px;
+}
+
+.emptyRow {
+ padding: 16px;
+ text-align: center;
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+}
+
+.message {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+}
+
+.error {
+ color: var(--error);
+}
diff --git a/components/admin/ProjectDetail.tsx b/components/admin/ProjectDetail.tsx
new file mode 100644
index 00000000..fb538644
--- /dev/null
+++ b/components/admin/ProjectDetail.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import Link from "next/link";
+import useSWR from "swr";
+import { ArrowLeft } from "lucide-react";
+import { ProjectRole } from "../../src/generated/client/browser";
+import styles from "./ProjectDetail.module.css";
+
+type ProjectDetailPayload = {
+ id: string;
+ title: string;
+ author: string | null;
+ description: string | null;
+ createdAt: string;
+ updatedAt: string;
+ _count: { members: number; invitations: number };
+};
+
+type Member = {
+ role: ProjectRole;
+ user: { id: string; email: string };
+};
+
+type Invite = {
+ email: string;
+ createdAt: string;
+};
+
+function formatDateTime(iso: string) {
+ return new Date(iso).toLocaleString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+type Props = { projectId: string };
+
+export default function ProjectDetail({ projectId }: Props) {
+ const { data, error, isLoading } = useSWR(
+ `/api/admin/projects/${projectId}`,
+ );
+ const { data: members } = useSWR(
+ `/api/admin/projects/${projectId}/members`,
+ );
+ const { data: invites } = useSWR(
+ `/api/admin/projects/${projectId}/invites`,
+ );
+
+ return (
+
+
+
Back to search
+
+
+ {isLoading &&
Loading project…
}
+ {error &&
Project not found.
}
+
+ {data && (
+ <>
+
+ Project
+
+
+ Title
+ {data.title}
+
+
+ Project ID
+
+ {data.id}
+
+
+
+ Author
+ {data.author ?? "—"}
+
+
+ Description
+ {data.description ?? "—"}
+
+
+ Created
+
+ {formatDateTime(data.createdAt)}
+
+
+
+ Last updated
+
+ {formatDateTime(data.updatedAt)}
+
+
+
+
+
+
+
+ Members ({data._count.members})
+
+
+
+ Email
+ Role
+
+ {!members && (
+
Loading…
+ )}
+ {members && members.length === 0 && (
+
No members.
+ )}
+ {members?.map((m) => (
+
+
{m.user.email}
+
{m.role}
+
+ ))}
+
+
+
+
+
+ Pending invitations ({data._count.invitations})
+
+
+
+ Email
+ Invited
+
+ {!invites && (
+
Loading…
+ )}
+ {invites && invites.length === 0 && (
+
No pending invitations.
+ )}
+ {invites?.map((inv) => (
+
+ {inv.email}
+
+ {formatDateTime(inv.createdAt)}
+
+
+ ))}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/components/admin/ProjectSearch.module.css b/components/admin/ProjectSearch.module.css
new file mode 100644
index 00000000..484ecf66
--- /dev/null
+++ b/components/admin/ProjectSearch.module.css
@@ -0,0 +1,104 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.searchBar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: var(--secondary);
+ border: 1px solid var(--input-outline);
+ border-radius: 10px;
+ padding: 10px 14px;
+ box-shadow: var(--panel-shadow);
+}
+
+.searchBar:focus-within {
+ border-color: var(--primary-text);
+}
+
+.searchInput {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+}
+
+.hint {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ padding: 4px 2px;
+}
+
+.resultList {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--separator);
+ border-radius: 10px;
+ overflow: hidden;
+ background: var(--secondary);
+}
+
+.resultRow {
+ display: grid;
+ grid-template-columns: 2fr 1fr auto auto;
+ gap: 16px;
+ align-items: center;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--separator);
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ text-decoration: none;
+ transition: background-color 120ms ease;
+}
+
+.resultRow:last-child {
+ border-bottom: none;
+}
+
+.resultRow:hover {
+ background: var(--primary-hover);
+}
+
+.title {
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.id {
+ color: var(--secondary-text);
+ font-family: var(--font-courier), monospace;
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.members {
+ color: var(--secondary-text);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.date {
+ color: var(--secondary-text);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.empty {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ padding: 24px;
+ text-align: center;
+}
diff --git a/components/admin/ProjectSearch.tsx b/components/admin/ProjectSearch.tsx
new file mode 100644
index 00000000..7ae94a5d
--- /dev/null
+++ b/components/admin/ProjectSearch.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import useSWR from "swr";
+import Link from "next/link";
+import { Search } from "lucide-react";
+import styles from "./ProjectSearch.module.css";
+
+type ProjectResult = {
+ id: string;
+ title: string;
+ author: string | null;
+ createdAt: string;
+ updatedAt: string;
+ _count: { members: number };
+};
+
+type SearchResponse = {
+ projects: ProjectResult[];
+ nextCursor: number | null;
+};
+
+function formatDate(iso: string) {
+ return new Date(iso).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+}
+
+export default function ProjectSearch() {
+ const [query, setQuery] = useState("");
+ const [debounced, setDebounced] = useState("");
+
+ useEffect(() => {
+ const t = setTimeout(() => setDebounced(query.trim()), 250);
+ return () => clearTimeout(t);
+ }, [query]);
+
+ const key = debounced ? `/api/admin/projects?q=${encodeURIComponent(debounced)}` : null;
+ const { data, isLoading } = useSWR(key);
+
+ return (
+
+
+
+ setQuery(e.target.value)}
+ autoFocus
+ />
+
+
+ {!debounced && (
+
+ Start typing a title (partial, case-insensitive) or paste a full project ID.
+
+ )}
+
+ {debounced && isLoading &&
Searching…
}
+
+ {debounced && data && data.projects.length === 0 && (
+
+
No projects match "{debounced}".
+
+ )}
+
+ {data && data.projects.length > 0 && (
+
+ {data.projects.map((p) => (
+
+ {p.title}
+ {p.id}
+
+ {p._count.members} member{p._count.members !== 1 ? "s" : ""}
+
+
+ Updated {formatDate(p.updatedAt)}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/admin/StatsCards.module.css b/components/admin/StatsCards.module.css
new file mode 100644
index 00000000..241bc100
--- /dev/null
+++ b/components/admin/StatsCards.module.css
@@ -0,0 +1,51 @@
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px;
+}
+
+.card {
+ background: var(--secondary);
+ border: 1px solid var(--separator);
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: var(--panel-shadow);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.label {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 12px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ font-weight: 600;
+}
+
+.value {
+ color: var(--primary-text);
+ font-family: var(--font-josefin), sans-serif;
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.hint {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 12px;
+}
+
+.loading,
+.error {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ padding: 12px 4px;
+}
+
+.error {
+ color: var(--error);
+}
diff --git a/components/admin/StatsCards.tsx b/components/admin/StatsCards.tsx
new file mode 100644
index 00000000..921bae90
--- /dev/null
+++ b/components/admin/StatsCards.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import useSWR from "swr";
+import styles from "./StatsCards.module.css";
+
+type Stats = {
+ userCount: number;
+ activeProCount: number;
+ projectCount: number;
+ transactionsThisMonth: number;
+};
+
+export default function StatsCards() {
+ const { data, error, isLoading } = useSWR("/api/admin/stats");
+
+ if (isLoading) return Loading stats…
;
+ if (error || !data) return Failed to load stats.
;
+
+ const cards = [
+ { label: "Users", value: data.userCount, hint: "All-time registrations" },
+ { label: "Active Pro", value: data.activeProCount, hint: "isProUntil > now" },
+ { label: "Projects", value: data.projectCount, hint: "All projects" },
+ {
+ label: "Transactions (this month)",
+ value: data.transactionsThisMonth,
+ hint: "Since the 1st",
+ },
+ ];
+
+ return (
+
+ {cards.map((c) => (
+
+ {c.label}
+ {c.value.toLocaleString()}
+ {c.hint}
+
+ ))}
+
+ );
+}
diff --git a/components/admin/UserDetail.module.css b/components/admin/UserDetail.module.css
new file mode 100644
index 00000000..62455ea1
--- /dev/null
+++ b/components/admin/UserDetail.module.css
@@ -0,0 +1,177 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.backLink {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--secondary-text);
+ text-decoration: none;
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ margin-bottom: 8px;
+ width: fit-content;
+}
+
+.backLink:hover {
+ color: var(--primary-text);
+}
+
+.card {
+ background: var(--secondary);
+ border: 1px solid var(--separator);
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: var(--panel-shadow);
+}
+
+.cardTitle {
+ color: var(--primary-text);
+ font-family: var(--font-josefin), sans-serif;
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 16px;
+}
+
+.fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px 24px;
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.fieldLabel {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.fieldValue {
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ overflow-wrap: anywhere;
+}
+
+.idValue {
+ font-family: var(--font-courier), monospace;
+ font-size: 13px;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 3px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ width: fit-content;
+}
+
+.badgePro {
+ background: color-mix(in srgb, var(--success) 22%, transparent);
+ color: var(--success);
+}
+
+.badgeAdmin {
+ background: color-mix(in srgb, var(--warning) 25%, transparent);
+ color: var(--warning);
+}
+
+.badgeMuted {
+ background: var(--tertiary);
+ color: var(--secondary-text);
+}
+
+.badgeDanger {
+ background: color-mix(in srgb, var(--error) 22%, transparent);
+ color: var(--error);
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--separator);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.tableRow {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr;
+ gap: 16px;
+ padding: 12px 16px;
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ color: var(--primary-text);
+ border-bottom: 1px solid var(--separator);
+ align-items: center;
+}
+
+.tableRow:last-child {
+ border-bottom: none;
+}
+
+.tableHeader {
+ background: var(--primary);
+ color: var(--secondary-text);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ font-weight: 600;
+}
+
+.tableRowLink {
+ text-decoration: none;
+ transition: background-color 120ms ease;
+ cursor: pointer;
+}
+
+.tableRowLink:hover {
+ background: var(--primary-hover);
+}
+
+.tableCellMuted {
+ color: var(--secondary-text);
+ font-size: 12px;
+}
+
+.tableCellMono {
+ font-family: var(--font-courier), monospace;
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.emptyRow {
+ padding: 16px;
+ text-align: center;
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+}
+
+.message {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+}
+
+.error {
+ color: var(--error);
+}
diff --git a/components/admin/UserDetail.tsx b/components/admin/UserDetail.tsx
new file mode 100644
index 00000000..4e885669
--- /dev/null
+++ b/components/admin/UserDetail.tsx
@@ -0,0 +1,242 @@
+"use client";
+
+import Link from "next/link";
+import useSWR from "swr";
+import { ArrowLeft } from "lucide-react";
+import { UserRole, SubscriptionProvider, ProjectRole } from "../../src/generated/client/browser";
+import styles from "./UserDetail.module.css";
+
+type UserDetailPayload = {
+ user: {
+ id: string;
+ email: string;
+ createdAt: string;
+ emailVerified: string | null;
+ username: string | null;
+ role: UserRole;
+ isProUntil: string | null;
+ isSubscriptionCancelled: boolean;
+ subscriptionProvider: SubscriptionProvider | null;
+ };
+ projectCount: number;
+ transactionCount: number;
+};
+
+type Membership = {
+ role: ProjectRole;
+ project: {
+ id: string;
+ title: string;
+ createdAt: string;
+ updatedAt: string;
+ };
+};
+
+type Transaction = {
+ id: number;
+ provider: SubscriptionProvider;
+ transactionId: string;
+ createdAt: string;
+};
+
+function formatDateTime(iso: string | null) {
+ if (!iso) return "—";
+ const d = new Date(iso);
+ return d.toLocaleString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+function isPro(isProUntil: string | null) {
+ return !!isProUntil && new Date(isProUntil) > new Date();
+}
+
+type Props = { userId: string };
+
+export default function UserDetail({ userId }: Props) {
+ const { data, error, isLoading } = useSWR(
+ `/api/admin/users/${userId}`,
+ );
+ const { data: memberships } = useSWR(
+ `/api/admin/users/${userId}/projects`,
+ );
+ const { data: transactions } = useSWR(
+ `/api/admin/users/${userId}/transactions`,
+ );
+
+ return (
+
+
+
Back to search
+
+
+ {isLoading &&
Loading user…
}
+ {error &&
User not found.
}
+
+ {data && (
+ <>
+
+ Account
+
+
+ Email
+ {data.user.email}
+
+
+ User ID
+
+ {data.user.id}
+
+
+
+ Username
+
+ {data.user.username ?? "—"}
+
+
+
+ Created
+
+ {formatDateTime(data.user.createdAt)}
+
+
+
+ Email verified
+
+ {data.user.emailVerified
+ ? formatDateTime(data.user.emailVerified)
+ : "No"}
+
+
+
+ Role
+
+ {data.user.role}
+
+
+
+
+
+
+ Subscription
+
+
+ Status
+
+ {isPro(data.user.isProUntil) ? "Pro" : "Free"}
+
+
+
+ Pro until
+
+ {formatDateTime(data.user.isProUntil)}
+
+
+
+ Provider
+
+ {data.user.subscriptionProvider ?? "—"}
+
+
+
+ Cancelled
+
+ {data.user.isSubscriptionCancelled ? "Yes" : "No"}
+
+
+
+
+
+
+
+ Projects ({data.projectCount})
+
+
+
+ Title
+ Role
+ Updated
+
+ {!memberships && (
+
Loading…
+ )}
+ {memberships && memberships.length === 0 && (
+
+ This user has no projects.
+
+ )}
+ {memberships?.map((m) => (
+
+
{m.project.title}
+
{m.role}
+
+ {formatDateTime(m.project.updatedAt)}
+
+
+ ))}
+
+
+
+
+
+ Transactions ({data.transactionCount})
+
+
+
+ Transaction ID
+ Provider
+ Created
+
+ {!transactions && (
+
Loading…
+ )}
+ {transactions && transactions.length === 0 && (
+
No transactions.
+ )}
+ {transactions?.map((t) => (
+
+
+ {t.transactionId}
+
+ {t.provider}
+
+ {formatDateTime(t.createdAt)}
+
+
+ ))}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/components/admin/UserSearch.module.css b/components/admin/UserSearch.module.css
new file mode 100644
index 00000000..69093ed7
--- /dev/null
+++ b/components/admin/UserSearch.module.css
@@ -0,0 +1,128 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.searchBar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: var(--secondary);
+ border: 1px solid var(--input-outline);
+ border-radius: 10px;
+ padding: 10px 14px;
+ box-shadow: var(--panel-shadow);
+}
+
+.searchBar:focus-within {
+ border-color: var(--primary-text);
+}
+
+.searchInput {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+}
+
+.hint {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 13px;
+ padding: 4px 2px;
+}
+
+.resultList {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--separator);
+ border-radius: 10px;
+ overflow: hidden;
+ background: var(--secondary);
+}
+
+.resultRow {
+ display: grid;
+ grid-template-columns: 1.5fr 1fr auto auto;
+ gap: 16px;
+ align-items: center;
+ padding: 14px 18px;
+ border: none;
+ border-bottom: 1px solid var(--separator);
+ background: transparent;
+ color: var(--primary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ text-align: left;
+ text-decoration: none;
+ cursor: pointer;
+ transition: background-color 120ms ease;
+}
+
+.resultRow:last-child {
+ border-bottom: none;
+}
+
+.resultRow:hover {
+ background: var(--primary-hover);
+}
+
+.email {
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.id {
+ color: var(--secondary-text);
+ font-family: var(--font-courier), monospace;
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 3px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.badgePro {
+ background: color-mix(in srgb, var(--success) 22%, transparent);
+ color: var(--success);
+}
+
+.badgeAdmin {
+ background: color-mix(in srgb, var(--warning) 25%, transparent);
+ color: var(--warning);
+}
+
+.badgeMuted {
+ background: var(--tertiary);
+ color: var(--secondary-text);
+}
+
+.date {
+ color: var(--secondary-text);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.empty {
+ color: var(--secondary-text);
+ font-family: var(--font-inter), sans-serif;
+ font-size: 14px;
+ padding: 24px;
+ text-align: center;
+}
diff --git a/components/admin/UserSearch.tsx b/components/admin/UserSearch.tsx
new file mode 100644
index 00000000..ad5c8499
--- /dev/null
+++ b/components/admin/UserSearch.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import useSWR from "swr";
+import Link from "next/link";
+import { Search } from "lucide-react";
+import { UserRole, SubscriptionProvider } from "../../src/generated/client/browser";
+import styles from "./UserSearch.module.css";
+
+type SearchResult = {
+ id: string;
+ email: string;
+ createdAt: string;
+ role: UserRole;
+ isProUntil: string | null;
+ subscriptionProvider: SubscriptionProvider | null;
+};
+
+type SearchResponse = {
+ users: SearchResult[];
+ nextCursor: number | null;
+};
+
+function formatDate(iso: string) {
+ const d = new Date(iso);
+ return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
+}
+
+function isPro(isProUntil: string | null) {
+ return !!isProUntil && new Date(isProUntil) > new Date();
+}
+
+export default function UserSearch() {
+ const [query, setQuery] = useState("");
+ const [debounced, setDebounced] = useState("");
+
+ useEffect(() => {
+ const t = setTimeout(() => setDebounced(query.trim()), 250);
+ return () => clearTimeout(t);
+ }, [query]);
+
+ const key = debounced ? `/api/admin/users?q=${encodeURIComponent(debounced)}` : null;
+ const { data, isLoading } = useSWR(key);
+
+ return (
+
+
+
+ setQuery(e.target.value)}
+ autoFocus
+ />
+
+
+ {!debounced && (
+
+ Start typing an email (partial, case-insensitive) or paste a full user ID.
+
+ )}
+
+ {debounced && isLoading &&
Searching…
}
+
+ {debounced && data && data.users.length === 0 && (
+
+
No users match "{debounced}".
+
+ )}
+
+ {data && data.users.length > 0 && (
+
+ {data.users.map((u) => (
+
+ {u.email}
+ {u.id}
+
+ {u.role === UserRole.ADMIN
+ ? "Admin"
+ : isPro(u.isProUntil)
+ ? "Pro"
+ : "Free"}
+
+ {formatDate(u.createdAt)}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/dashboard/project/CollaboratorsSettings.tsx b/components/dashboard/project/CollaboratorsSettings.tsx
index 05c3d6e7..7d98ce24 100644
--- a/components/dashboard/project/CollaboratorsSettings.tsx
+++ b/components/dashboard/project/CollaboratorsSettings.tsx
@@ -4,7 +4,7 @@ import { useContext, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { useCookieUser, useIsPro, useProjectCollaborators, useProjectInvites, useProjectMembership } from "@src/lib/utils/hooks";
import { CookieUser } from "@src/lib/utils/types";
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../src/generated/client/browser";
import { Collaborator, ProjectInvite, ProjectMembershipPayload } from "@src/server/repository/project-repository";
import { Info, Lock } from "lucide-react";
diff --git a/docker-compose.yml b/docker-compose.yml
index 9a4ae7f7..ab348deb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -114,6 +114,71 @@ services:
timeout: 5s
retries: 5
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: scriptio-prometheus
+ profiles: ["prod"]
+ restart: unless-stopped
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.retention.time=30d
+ volumes:
+ - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - prometheus_data:/prometheus
+ configs:
+ - source: metrics_bearer_token
+ target: /etc/prometheus/bearer-token
+ mode: 0400
+ networks:
+ - internal
+ depends_on:
+ - app-prod
+
+ grafana:
+ image: grafana/grafana:latest
+ container_name: scriptio-grafana
+ profiles: ["prod"]
+ restart: unless-stopped
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
+ - GF_SERVER_ROOT_URL=https://monitoring.scriptio.app
+ - GF_USERS_ALLOW_SIGN_UP=false
+ - GF_SMTP_ENABLED=true
+ - GF_SMTP_HOST=${SMTP_HOST}:587
+ - GF_SMTP_USER=${SMTP_USER}
+ - GF_SMTP_PASSWORD=${SMTP_SECRET}
+ - GF_SMTP_FROM_ADDRESS=noreply@scriptio.app
+ - GF_SMTP_FROM_NAME=Scriptio Monitoring
+ - APP_MEMORY_LIMIT_BYTES=${APP_MEMORY_LIMIT_BYTES}
+ - DB_SIZE_LIMIT_BYTES=${DB_SIZE_LIMIT_BYTES}
+ volumes:
+ - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
+ - ./monitoring/grafana/dashboards:/etc/grafana/dashboards:ro
+ - grafana_data:/var/lib/grafana
+ networks:
+ - webproxy
+ - internal
+ depends_on:
+ - prometheus
+ labels:
+ - traefik.enable=true
+ - traefik.docker.network=webproxy
+ - traefik.http.routers.scriptio-monitoring.rule=Host(`monitoring.scriptio.app`)
+ - traefik.http.routers.scriptio-monitoring.entrypoints=secure
+ - traefik.http.routers.scriptio-monitoring.tls=true
+ - traefik.http.routers.scriptio-monitoring.tls.certresolver=letsencrypt
+ - traefik.http.routers.scriptio-monitoring.middlewares=monitoring-auth
+ - traefik.http.middlewares.monitoring-auth.basicauth.users=${MONITORING_AUTH_USERS}
+ - traefik.http.services.scriptio-monitoring.loadbalancer.server.port=3000
+
+configs:
+ metrics_bearer_token:
+ content: ${METRICS_BEARER_TOKEN}
+
+volumes:
+ prometheus_data:
+ grafana_data:
+
networks:
webproxy:
external: true
diff --git a/monitoring/grafana/dashboards/scriptio-overview.json b/monitoring/grafana/dashboards/scriptio-overview.json
new file mode 100644
index 00000000..351ccb6f
--- /dev/null
+++ b/monitoring/grafana/dashboards/scriptio-overview.json
@@ -0,0 +1,102 @@
+{
+ "uid": "scriptio-overview",
+ "title": "Scriptio Overview",
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "refresh": "30s",
+ "time": { "from": "now-6h", "to": "now" },
+ "templating": {
+ "list": [
+ {
+ "name": "memory_limit_bytes",
+ "type": "constant",
+ "label": "Memory limit (bytes)",
+ "query": "${APP_MEMORY_LIMIT_BYTES}",
+ "current": { "value": "${APP_MEMORY_LIMIT_BYTES}", "text": "${APP_MEMORY_LIMIT_BYTES}" },
+ "hide": 2
+ },
+ {
+ "name": "db_size_limit_bytes",
+ "type": "constant",
+ "label": "DB size limit (bytes)",
+ "query": "${DB_SIZE_LIMIT_BYTES}",
+ "current": { "value": "${DB_SIZE_LIMIT_BYTES}", "text": "${DB_SIZE_LIMIT_BYTES}" },
+ "hide": 2
+ }
+ ]
+ },
+ "panels": [
+ {
+ "id": 1,
+ "type": "timeseries",
+ "title": "Database size",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
+ "fieldConfig": {
+ "defaults": { "unit": "bytes" },
+ "overrides": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "scriptio_db_size_bytes",
+ "legendFormat": "DB size"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "type": "timeseries",
+ "title": "HTTP error rate (4xx + 5xx)",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
+ "fieldConfig": {
+ "defaults": { "unit": "percentunit", "min": 0, "max": 1 },
+ "overrides": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "sum(rate(http_requests_total{status=~\"4..|5..\"}[5m])) / sum(rate(http_requests_total[5m]))",
+ "legendFormat": "error rate"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "type": "timeseries",
+ "title": "CPU usage (% of one core)",
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
+ "fieldConfig": {
+ "defaults": { "unit": "percent", "min": 0 },
+ "overrides": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "rate(process_cpu_seconds_total[2m]) * 100",
+ "legendFormat": "cpu %"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "type": "timeseries",
+ "title": "Memory usage (% of APP_MEMORY_LIMIT_BYTES)",
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
+ "fieldConfig": {
+ "defaults": { "unit": "percent", "min": 0, "max": 100 },
+ "overrides": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "process_resident_memory_bytes / $memory_limit_bytes * 100",
+ "legendFormat": "memory %"
+ }
+ ]
+ }
+ ]
+}
diff --git a/monitoring/grafana/provisioning/alerting/contact-points.yml b/monitoring/grafana/provisioning/alerting/contact-points.yml
new file mode 100644
index 00000000..f2f16f36
--- /dev/null
+++ b/monitoring/grafana/provisioning/alerting/contact-points.yml
@@ -0,0 +1,11 @@
+apiVersion: 1
+
+contactPoints:
+ - orgId: 1
+ name: scriptio-email
+ receivers:
+ - uid: scriptio-email
+ type: email
+ settings:
+ addresses: hugo.bois@hotmail.fr
+ singleEmail: true
diff --git a/monitoring/grafana/provisioning/alerting/notification-policies.yml b/monitoring/grafana/provisioning/alerting/notification-policies.yml
new file mode 100644
index 00000000..bcb35290
--- /dev/null
+++ b/monitoring/grafana/provisioning/alerting/notification-policies.yml
@@ -0,0 +1,9 @@
+apiVersion: 1
+
+policies:
+ - orgId: 1
+ receiver: scriptio-email
+ group_by: [alertname]
+ group_wait: 30s
+ group_interval: 5m
+ repeat_interval: 6h
diff --git a/monitoring/grafana/provisioning/alerting/rules.yml b/monitoring/grafana/provisioning/alerting/rules.yml
new file mode 100644
index 00000000..c78106dc
--- /dev/null
+++ b/monitoring/grafana/provisioning/alerting/rules.yml
@@ -0,0 +1,73 @@
+apiVersion: 1
+
+groups:
+ - orgId: 1
+ name: scriptio-app
+ folder: scriptio
+ interval: 1m
+ rules:
+ - uid: scriptio-http-error-rate
+ title: HighHttpErrorRate
+ condition: C
+ data:
+ - refId: A
+ relativeTimeRange:
+ from: 600
+ to: 0
+ datasourceUid: prometheus
+ model:
+ refId: A
+ expr: sum(rate(http_requests_total{status=~"4..|5.."}[5m])) / sum(rate(http_requests_total[5m]))
+ instant: true
+ - refId: C
+ relativeTimeRange:
+ from: 600
+ to: 0
+ datasourceUid: __expr__
+ model:
+ refId: C
+ type: threshold
+ expression: A
+ conditions:
+ - evaluator:
+ type: gt
+ params: [0.05]
+ for: 5m
+ noDataState: OK
+ execErrState: Error
+ annotations:
+ summary: HTTP error rate above 5%
+ description: "{{ $value | printf \"%.2f\" }} of requests in the last 5 minutes returned a 4xx/5xx status."
+
+ - uid: scriptio-db-almost-full
+ title: DbAlmostFull
+ condition: C
+ data:
+ - refId: A
+ relativeTimeRange:
+ from: 600
+ to: 0
+ datasourceUid: prometheus
+ model:
+ refId: A
+ expr: scriptio_db_size_bytes / ${DB_SIZE_LIMIT_BYTES}
+ instant: true
+ - refId: C
+ relativeTimeRange:
+ from: 600
+ to: 0
+ datasourceUid: __expr__
+ model:
+ refId: C
+ type: threshold
+ expression: A
+ conditions:
+ - evaluator:
+ type: gt
+ params: [0.85]
+ for: 10m
+ noDataState: OK
+ execErrState: Error
+ annotations:
+ summary: Database is approaching its capacity ceiling
+ description: "DB is at {{ $value | humanizePercentage }} of the configured DB_SIZE_LIMIT_BYTES."
diff --git a/monitoring/grafana/provisioning/dashboards/dashboards.yml b/monitoring/grafana/provisioning/dashboards/dashboards.yml
new file mode 100644
index 00000000..cba8dccc
--- /dev/null
+++ b/monitoring/grafana/provisioning/dashboards/dashboards.yml
@@ -0,0 +1,13 @@
+apiVersion: 1
+
+providers:
+ - name: scriptio
+ orgId: 1
+ folder: ""
+ type: file
+ disableDeletion: true
+ updateIntervalSeconds: 30
+ allowUiUpdates: false
+ options:
+ path: /etc/grafana/dashboards
+ foldersFromFilesStructure: false
diff --git a/monitoring/grafana/provisioning/datasources/prometheus.yml b/monitoring/grafana/provisioning/datasources/prometheus.yml
new file mode 100644
index 00000000..8967eb69
--- /dev/null
+++ b/monitoring/grafana/provisioning/datasources/prometheus.yml
@@ -0,0 +1,10 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ uid: prometheus
+ type: prometheus
+ access: proxy
+ url: http://prometheus:9090
+ isDefault: true
+ editable: false
diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml
new file mode 100644
index 00000000..ea137064
--- /dev/null
+++ b/monitoring/prometheus/prometheus.yml
@@ -0,0 +1,13 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: app-prod
+ metrics_path: /api/metrics
+ scheme: http
+ authorization:
+ type: Bearer
+ credentials_file: /etc/prometheus/bearer-token
+ static_configs:
+ - targets: ["app-prod:3000"]
diff --git a/next.config.ts b/next.config.ts
index 7fac6fcd..6f17229b 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -22,4 +22,4 @@ const config: NextConfig = {
},
};
-module.exports = config;
+export default config;
diff --git a/package-lock.json b/package-lock.json
index 0a60567c..8ff438d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,13 +14,13 @@
"@aws-sdk/s3-request-presigner": "^3.952.0",
"@choochmeque/tauri-plugin-iap-api": "^0.8.2",
"@formkit/auto-animate": "^0.7.0",
- "@prisma/client": "^6.19.0",
+ "@prisma/adapter-pg": "^7.8.0",
+ "@prisma/client": "^7.7.0",
"@svgr/core": "^8.1.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-fs": "2.4.5",
"@tauri-apps/plugin-opener": "2.5.3",
- "@tauri-apps/plugin-sql": "2.3.2",
"@tauri-apps/plugin-store": "^2.4.2",
"@tiptap/extension-bold": "^3.13.0",
"@tiptap/extension-collaboration": "^3.13.0",
@@ -52,6 +52,8 @@
"next-themes": "^0.2.0",
"nodemailer": "^7.0.12",
"pdfmake": "^0.2.9",
+ "pg": "^8.20.0",
+ "prom-client": "^15.1.3",
"react": "^19.2.4",
"react-chartjs-2": "^5.2.0",
"react-dom": "^19.2.4",
@@ -77,6 +79,7 @@
"@types/node": "20.11.0",
"@types/nodemailer": "^6.4.14",
"@types/pdfmake": "^0.2.12",
+ "@types/pg": "^8.20.0",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/uuid": "^10.0.0",
@@ -91,9 +94,9 @@
"eslint-plugin-react-hooks": "^7.0.1",
"patch-package": "^8.0.1",
"playwright": "^1.58.2",
- "prisma": "^6.19.0",
+ "prisma": "^7.7.0",
"tsx": "^4.21.0",
- "typescript": "^5.1.3",
+ "typescript": "^5.7.3",
"typescript-plugin-css-modules": "^5.2.0",
"vite": "^6.4.1",
"vitest": "^3.2.4",
@@ -3056,6 +3059,36 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@electric-sql/pglite": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
+ "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@electric-sql/pglite-socket": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz",
+ "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "pglite-server": "dist/scripts/server.js"
+ },
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.1"
+ }
+ },
+ "node_modules/@electric-sql/pglite-tools": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz",
+ "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.1"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@@ -3904,6 +3937,19 @@
"@hapi/hoek": "^11.0.2"
}
},
+ "node_modules/@hono/node-server": {
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
+ "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -5320,6 +5366,15 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
@@ -5691,18 +5746,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@prisma/adapter-pg": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz",
+ "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/driver-adapter-utils": "7.8.0",
+ "@types/pg": "^8.16.0",
+ "pg": "^8.16.3",
+ "postgres-array": "3.0.4"
+ }
+ },
"node_modules/@prisma/client": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.1.tgz",
- "integrity": "sha512-4SXj4Oo6HyQkLUWT8Ke5R0PTAfVOKip5Roo+6+b2EDTkFg5be0FnBWiuRJc0BC0sRQIWGMLKW1XguhVfW/z3/A==",
- "hasInstallScript": true,
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.7.0.tgz",
+ "integrity": "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==",
"license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/client-runtime-utils": "7.7.0"
+ },
"engines": {
- "node": ">=18.18"
+ "node": "^20.19 || ^22.12 || >=24.0"
},
"peerDependencies": {
"prisma": "*",
- "typescript": ">=5.1.0"
+ "typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"prisma": {
@@ -5713,67 +5782,363 @@
}
}
},
+ "node_modules/@prisma/client-runtime-utils": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.7.0.tgz",
+ "integrity": "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/@prisma/config": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.1.tgz",
- "integrity": "sha512-bUL/aYkGXLwxVGhJmQMtslLT7KPEfUqmRa919fKI4wQFX4bIFUKiY8Jmio/2waAjjPYrtuDHa7EsNCnJTXxiOw==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.7.0.tgz",
+ "integrity": "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
- "effect": "3.18.4",
+ "effect": "3.20.0",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.1.tgz",
- "integrity": "sha512-h1JImhlAd/s5nhY/e9qkAzausWldbeT+e4nZF7A4zjDYBF4BZmKDt4y0jK7EZapqOm1kW7V0e9agV/iFDy3fWw==",
- "devOptional": true,
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz",
+ "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==",
"license": "Apache-2.0"
},
+ "node_modules/@prisma/dev": {
+ "version": "0.24.3",
+ "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz",
+ "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "@electric-sql/pglite": "0.4.1",
+ "@electric-sql/pglite-socket": "0.1.1",
+ "@electric-sql/pglite-tools": "0.3.1",
+ "@hono/node-server": "1.19.11",
+ "@prisma/get-platform": "7.2.0",
+ "@prisma/query-plan-executor": "7.2.0",
+ "@prisma/streams-local": "0.1.2",
+ "foreground-child": "3.3.1",
+ "get-port-please": "3.2.0",
+ "hono": "^4.12.8",
+ "http-status-codes": "2.3.0",
+ "pathe": "2.0.3",
+ "proper-lockfile": "4.1.2",
+ "remeda": "2.33.4",
+ "std-env": "3.10.0",
+ "valibot": "1.2.0",
+ "zeptomatch": "2.1.0"
+ }
+ },
+ "node_modules/@prisma/driver-adapter-utils": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz",
+ "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.8.0"
+ }
+ },
"node_modules/@prisma/engines": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.1.tgz",
- "integrity": "sha512-xy95dNJ7DiPf9IJ3oaVfX785nbFl7oNDzclUF+DIiJw6WdWCvPl0LPU0YqQLsrwv8N64uOQkH391ujo3wSo+Nw==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz",
+ "integrity": "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.19.1",
- "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
- "@prisma/fetch-engine": "6.19.1",
- "@prisma/get-platform": "6.19.1"
+ "@prisma/debug": "7.7.0",
+ "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "@prisma/fetch-engine": "7.7.0",
+ "@prisma/get-platform": "7.7.0"
}
},
"node_modules/@prisma/engines-version": {
- "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
- "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
+ "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz",
+ "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines/node_modules/@prisma/debug": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz",
+ "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==",
"devOptional": true,
"license": "Apache-2.0"
},
+ "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz",
+ "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0"
+ }
+ },
"node_modules/@prisma/fetch-engine": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.1.tgz",
- "integrity": "sha512-mmgcotdaq4VtAHO6keov3db+hqlBzQS6X7tR7dFCbvXjLVTxBYdSJFRWz+dq7F9p6dvWyy1X0v8BlfRixyQK6g==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.7.0.tgz",
+ "integrity": "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.7.0",
+ "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711",
+ "@prisma/get-platform": "7.7.0"
+ }
+ },
+ "node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz",
+ "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz",
+ "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.19.1",
- "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
- "@prisma/get-platform": "6.19.1"
+ "@prisma/debug": "7.7.0"
}
},
"node_modules/@prisma/get-platform": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.1.tgz",
- "integrity": "sha512-zsg44QUiQAnFUyh6Fbt7c9HjMXHwFTqtrgcX7DAZmRgnkPyYT7Sh8Mn8D5PuuDYNtMOYcpLGg576MLfIORsBYw==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
+ "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.2.0"
+ }
+ },
+ "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
+ "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/query-plan-executor": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
+ "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/streams-local": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz",
+ "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "better-result": "^2.7.0",
+ "env-paths": "^3.0.0",
+ "proper-lockfile": "^4.1.2"
+ },
+ "engines": {
+ "bun": ">=1.3.6",
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@prisma/streams-local/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@prisma/studio-core": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz",
+ "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.19.1"
+ "@radix-ui/react-toggle": "1.1.10",
+ "chart.js": "4.5.1"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0",
+ "pnpm": "8"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@prisma/studio-core/node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/studio-core/node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
"node_modules/@remirror/core-constants": {
@@ -7604,15 +7969,6 @@
"@tauri-apps/api": "^2.8.0"
}
},
- "node_modules/@tauri-apps/plugin-sql": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-sql/-/plugin-sql-2.3.2.tgz",
- "integrity": "sha512-4VDXhcKXVpyh5KKpnTGAn6q2DikPHH+TXGh9ZDQzULmG/JEz1RDvzQStgBJKddiukRbYEZ8CGIA2kskx+T+PpA==",
- "license": "MIT OR Apache-2.0",
- "dependencies": {
- "@tauri-apps/api": "^2.10.1"
- }
- },
"node_modules/@tauri-apps/plugin-store": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.2.tgz",
@@ -8060,7 +8416,6 @@
"version": "20.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -8104,6 +8459,17 @@
"@types/pdfkit": "*"
}
},
+ "node_modules/@types/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
"node_modules/@types/postcss-modules-local-by-default": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz",
@@ -9204,6 +9570,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
@@ -9309,6 +9685,19 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/better-result": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.2.tgz",
+ "integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/bintrees": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
+ "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
+ "license": "MIT"
+ },
"node_modules/blake3-wasm": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
@@ -9785,9 +10174,9 @@
"license": "MIT"
},
"node_modules/confbox": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
- "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"devOptional": true,
"license": "MIT"
},
@@ -9902,7 +10291,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -10229,9 +10618,9 @@
}
},
"node_modules/defu": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
- "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"devOptional": true,
"license": "MIT"
},
@@ -10244,6 +10633,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -10399,9 +10798,9 @@
}
},
"node_modules/effect": {
- "version": "3.18.4",
- "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
- "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
+ "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -10477,6 +10876,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/env-paths": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+ "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
@@ -11414,7 +11826,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@@ -11487,6 +11899,23 @@
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fast-xml-parser": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.5.tgz",
@@ -11660,11 +12089,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "license": "MIT",
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -11753,6 +12199,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -11796,6 +12252,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-port-please": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
+ "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -11996,9 +12459,23 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
+ "node_modules/grammex": {
+ "version": "3.1.12",
+ "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
+ "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/graphmatch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
+ "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -12118,6 +12595,16 @@
"hulk": "bin/hulk"
}
},
+ "node_modules/hono": {
+ "version": "4.12.14",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
+ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
"node_modules/href-content": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/href-content/-/href-content-2.0.3.tgz",
@@ -12172,6 +12659,13 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/http-status-codes": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
+ "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/hunspell-asm": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/hunspell-asm/-/hunspell-asm-4.0.2.tgz",
@@ -12705,6 +13199,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -12880,7 +13381,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
"node_modules/isomorphic.js": {
@@ -13386,6 +13887,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -13424,6 +13932,22 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
"node_modules/lucide-react": {
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
@@ -13680,6 +14204,57 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/mysql2": {
+ "version": "3.15.3",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
+ "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.1",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.0",
+ "long": "^5.2.1",
+ "lru.min": "^1.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
@@ -14025,25 +14600,30 @@
}
},
"node_modules/nypm": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
- "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
+ "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "citty": "^0.1.6",
- "consola": "^3.4.2",
+ "citty": "^0.2.0",
"pathe": "^2.0.3",
- "pkg-types": "^2.3.0",
- "tinyexec": "^1.0.1"
+ "tinyexec": "^1.0.2"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
- "node": "^14.16.0 || >=16.10.0"
+ "node": ">=18"
}
},
+ "node_modules/nypm/node_modules/citty": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
+ "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/oauth4webapi": {
"version": "3.8.5",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz",
@@ -14496,7 +15076,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -14612,6 +15192,104 @@
"license": "MIT",
"optional": true
},
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pg-types/node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
"node_modules/pick-util": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/pick-util/-/pick-util-1.1.5.tgz",
@@ -14877,6 +15555,59 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/postgres": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
+ "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
+ "devOptional": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/porsager"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
+ "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
@@ -14942,26 +15673,34 @@
"license": "MIT"
},
"node_modules/prisma": {
- "version": "6.19.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.1.tgz",
- "integrity": "sha512-XRfmGzh6gtkc/Vq3LqZJcS2884dQQW3UhPo6jNRoiTW95FFQkXFg8vkYEy6og+Pyv0aY7zRQ7Wn1Cvr56XjhQQ==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz",
+ "integrity": "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/config": "6.19.1",
- "@prisma/engines": "6.19.1"
+ "@prisma/config": "7.7.0",
+ "@prisma/dev": "0.24.3",
+ "@prisma/engines": "7.7.0",
+ "@prisma/studio-core": "0.27.3",
+ "mysql2": "3.15.3",
+ "postgres": "3.4.7"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
- "node": ">=18.18"
+ "node": "^20.19 || ^22.12 || >=24.0"
},
"peerDependencies": {
- "typescript": ">=5.1.0"
+ "better-sqlite3": ">=9.0.0",
+ "typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
+ "better-sqlite3": {
+ "optional": true
+ },
"typescript": {
"optional": true
}
@@ -14977,6 +15716,19 @@
"node": ">= 0.6.0"
}
},
+ "node_modules/prom-client": {
+ "version": "15.1.3",
+ "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
+ "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.4.0",
+ "tdigest": "^0.1.1"
+ },
+ "engines": {
+ "node": "^16 || ^18 || >=20"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -14989,6 +15741,25 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "devOptional": true,
+ "license": "ISC"
+ },
"node_modules/prosemirror-changeset": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
@@ -15498,6 +16269,16 @@
"regjsparser": "bin/parser"
}
},
+ "node_modules/remeda": {
+ "version": "2.33.4",
+ "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
+ "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/remeda"
+ }
+ },
"node_modules/remote-content": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remote-content/-/remote-content-4.0.1.tgz",
@@ -15522,6 +16303,16 @@
"range-parser": "^1.2.1"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/reserved-words": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz",
@@ -15569,6 +16360,16 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -15800,6 +16601,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==",
+ "devOptional": true
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -15909,7 +16716,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -15922,7 +16729,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -16011,6 +16818,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/simple-xml-to-json": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz",
@@ -16094,6 +16914,25 @@
"specificity": "bin/specificity"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -16122,7 +16961,7 @@
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/stop-iteration-iterator": {
@@ -16511,6 +17350,15 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/tdigest": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
+ "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
+ "license": "MIT",
+ "dependencies": {
+ "bintrees": "1.0.2"
+ }
+ },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
@@ -16542,9 +17390,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
- "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
+ "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -17051,7 +17899,6 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/unenv": {
@@ -17302,6 +18149,21 @@
"uuid": "dist-node/bin/uuid"
}
},
+ "node_modules/valibot": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
+ "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
@@ -18136,7 +18998,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -18390,6 +19252,15 @@
"sax": "^1.2.4"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/y-indexeddb": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.12.tgz",
@@ -18566,6 +19437,17 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/zeptomatch": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
+ "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "grammex": "^3.1.11",
+ "graphmatch": "^1.1.0"
+ }
+ },
"node_modules/zod": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
diff --git a/package.json b/package.json
index 3b0736cf..61e0385b 100644
--- a/package.json
+++ b/package.json
@@ -2,13 +2,14 @@
"name": "scriptio",
"private": true,
"version": "2.1.0",
+ "type": "module",
"scripts": {
"postinstall": "patch-package",
"dev": "docker compose --profile dev up -d --wait && npx prisma db push && next dev",
"build": "npx prisma generate && next build",
- "build:tauri": "npx prisma generate && npx tsx src/build-tauri.ts",
- "worker:dev": "wrangler dev -c src/lib/collaboration/wrangler.toml",
- "worker:deploy": "wrangler deploy -c src/lib/collaboration/wrangler.toml",
+ "build:tauri": "npx prisma generate && npx tsx scripts/build-tauri.ts",
+ "worker:dev": "wrangler dev -c src/lib/cloud/wrangler.toml",
+ "worker:deploy": "wrangler deploy -c src/lib/cloud/wrangler.toml",
"stripe:dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe",
"tauri": "tauri",
"tauri:dev": "tauri dev",
@@ -32,7 +33,8 @@
"@aws-sdk/s3-request-presigner": "^3.952.0",
"@choochmeque/tauri-plugin-iap-api": "^0.8.2",
"@formkit/auto-animate": "^0.7.0",
- "@prisma/client": "^6.19.0",
+ "@prisma/adapter-pg": "^7.8.0",
+ "@prisma/client": "^7.7.0",
"@svgr/core": "^8.1.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "2.6.0",
@@ -69,6 +71,8 @@
"next-themes": "^0.2.0",
"nodemailer": "^7.0.12",
"pdfmake": "^0.2.9",
+ "pg": "^8.20.0",
+ "prom-client": "^15.1.3",
"react": "^19.2.4",
"react-chartjs-2": "^5.2.0",
"react-dom": "^19.2.4",
@@ -94,6 +98,7 @@
"@types/node": "20.11.0",
"@types/nodemailer": "^6.4.14",
"@types/pdfmake": "^0.2.12",
+ "@types/pg": "^8.20.0",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/uuid": "^10.0.0",
@@ -108,9 +113,9 @@
"eslint-plugin-react-hooks": "^7.0.1",
"patch-package": "^8.0.1",
"playwright": "^1.58.2",
- "prisma": "^6.19.0",
+ "prisma": "^7.7.0",
"tsx": "^4.21.0",
- "typescript": "^5.1.3",
+ "typescript": "^5.7.3",
"typescript-plugin-css-modules": "^5.2.0",
"vite": "^6.4.1",
"vitest": "^3.2.4",
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 00000000..6bab5632
--- /dev/null
+++ b/prisma.config.ts
@@ -0,0 +1,14 @@
+import "dotenv/config";
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+ schema: "prisma/schema.prisma",
+ datasource: {
+ // env() throws if the variable is absent (even for generate-only commands).
+ // process.env with a fallback keeps prisma generate working in CI without a DB.
+ url: process.env.DATABASE_URL ?? "postgresql://localhost/build",
+ },
+ migrations: {
+ seed: "tsx prisma/seed.ts",
+ },
+});
diff --git a/prisma/migrations/20260422105711_init/migration.sql b/prisma/migrations/20260422105711_init/migration.sql
deleted file mode 100644
index 061247ac..00000000
--- a/prisma/migrations/20260422105711_init/migration.sql
+++ /dev/null
@@ -1,168 +0,0 @@
--- CreateEnum
-CREATE TYPE "ProjectRole" AS ENUM ('OWNER', 'ADMIN', 'EDITOR', 'VIEWER');
-
--- CreateEnum
-CREATE TYPE "SubscriptionProvider" AS ENUM ('STRIPE', 'APPLE');
-
--- CreateTable
-CREATE TABLE "User" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "email" TEXT NOT NULL,
- "emailVerified" TIMESTAMP(3),
- "username" TEXT,
- "color" TEXT,
- "isProUntil" TIMESTAMP(3),
- "isSubscriptionCancelled" BOOLEAN NOT NULL DEFAULT false,
- "subscriptionProvider" "SubscriptionProvider",
- "settings" JSONB,
-
- CONSTRAINT "User_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Account" (
- "id" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "type" TEXT NOT NULL,
- "provider" TEXT NOT NULL,
- "providerAccountId" TEXT NOT NULL,
- "refresh_token" TEXT,
- "access_token" TEXT,
- "expires_at" INTEGER,
- "token_type" TEXT,
- "scope" TEXT,
- "id_token" TEXT,
- "session_state" TEXT,
-
- CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Session" (
- "id" TEXT NOT NULL,
- "sessionToken" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "expires" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "VerificationToken" (
- "identifier" TEXT NOT NULL,
- "token" TEXT NOT NULL,
- "expires" TIMESTAMP(3) NOT NULL
-);
-
--- CreateTable
-CREATE TABLE "MagicLinkToken" (
- "id" SERIAL NOT NULL,
- "email" TEXT NOT NULL,
- "tokenHash" TEXT NOT NULL,
- "expiresAt" TIMESTAMP(3) NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "desktopNonce" TEXT,
- "inviteToken" TEXT,
-
- CONSTRAINT "MagicLinkToken_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Transaction" (
- "id" SERIAL NOT NULL,
- "userId" TEXT NOT NULL,
- "provider" "SubscriptionProvider" NOT NULL,
- "transactionId" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Project" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "title" TEXT NOT NULL,
- "description" TEXT,
- "author" TEXT,
- "hasPoster" BOOLEAN NOT NULL DEFAULT false,
-
- CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "ProjectMember" (
- "id" SERIAL NOT NULL,
- "role" "ProjectRole" NOT NULL DEFAULT 'VIEWER',
- "userId" TEXT NOT NULL,
- "projectId" TEXT NOT NULL,
-
- CONSTRAINT "ProjectMember_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "ProjectInvitation" (
- "id" SERIAL NOT NULL,
- "token" TEXT NOT NULL,
- "email" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "projectId" TEXT NOT NULL,
-
- CONSTRAINT "ProjectInvitation_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-
--- CreateIndex
-CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-
--- CreateIndex
-CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "MagicLinkToken_tokenHash_key" ON "MagicLinkToken"("tokenHash");
-
--- CreateIndex
-CREATE INDEX "MagicLinkToken_email_createdAt_idx" ON "MagicLinkToken"("email", "createdAt");
-
--- CreateIndex
-CREATE INDEX "Transaction_userId_idx" ON "Transaction"("userId");
-
--- CreateIndex
-CREATE INDEX "Transaction_transactionId_idx" ON "Transaction"("transactionId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "ProjectMember_userId_projectId_key" ON "ProjectMember"("userId", "projectId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "ProjectInvitation_token_key" ON "ProjectInvitation"("token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "ProjectInvitation_email_projectId_key" ON "ProjectInvitation"("email", "projectId");
-
--- AddForeignKey
-ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "ProjectInvitation" ADD CONSTRAINT "ProjectInvitation_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
deleted file mode 100644
index 044d57cd..00000000
--- a/prisma/migrations/migration_lock.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-# Please do not edit this file manually
-# It should be added in your version-control system (e.g., Git)
-provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 8115d82b..8ee20c7e 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -2,12 +2,12 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
- provider = "prisma-client-js"
+ provider = "prisma-client"
+ output = "../src/generated/client"
}
datasource db {
provider = "postgresql"
- url = env("DATABASE_URL")
}
enum ProjectRole {
@@ -17,6 +17,11 @@ enum ProjectRole {
VIEWER
}
+enum UserRole {
+ USER
+ ADMIN
+}
+
enum SubscriptionProvider {
STRIPE
APPLE
@@ -29,6 +34,7 @@ model User {
emailVerified DateTime?
username String?
color String?
+ role UserRole @default(USER)
isProUntil DateTime?
isSubscriptionCancelled Boolean @default(false)
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 00000000..1094a2ff
--- /dev/null
+++ b/prisma/seed.ts
@@ -0,0 +1,38 @@
+import "dotenv/config";
+import { PrismaClient, UserRole } from "../src/generated/client/client";
+import { PrismaPg } from "@prisma/adapter-pg";
+import pg from "pg";
+
+const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
+const adapter = new PrismaPg(pool);
+const prisma = new PrismaClient({ adapter });
+
+async function main() {
+ const adminEmail = process.env.SEED_ADMIN_EMAIL;
+
+ if (!adminEmail) {
+ console.log("No SEED_ADMIN_EMAIL set — skipping admin promotion.");
+ return;
+ }
+
+ const user = await prisma.user.upsert({
+ where: { email: adminEmail },
+ update: { role: UserRole.ADMIN },
+ create: {
+ email: adminEmail,
+ emailVerified: new Date(),
+ role: UserRole.ADMIN,
+ },
+ });
+
+ console.log(`Admin set: ${user.email} (id: ${user.id})`);
+}
+
+main()
+ .catch((e) => {
+ console.error(e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
diff --git a/src/build-tauri.ts b/scripts/build-tauri.ts
similarity index 53%
rename from src/build-tauri.ts
rename to scripts/build-tauri.ts
index fe6d8f92..b4a57872 100644
--- a/src/build-tauri.ts
+++ b/scripts/build-tauri.ts
@@ -3,20 +3,28 @@ import { join } from "path";
import { execSync } from "child_process";
const apiDir = join("src", "app", "api");
-const hiddenDir = join("src", "app", "_api");
+const hiddenApiDir = join("src", "app", "_api");
+const adminDir = join("src", "app", "admin");
+const hiddenAdminDir = join("src", "app", "_admin");
// Clean .next cache to avoid stale type references to API routes
rmSync(".next", { recursive: true, force: true });
// Prefix with _ so Next.js ignores the API routes during static export
if (existsSync(apiDir)) {
- renameSync(apiDir, hiddenDir);
+ renameSync(apiDir, hiddenApiDir);
+}
+if (existsSync(adminDir)) {
+ renameSync(adminDir, hiddenAdminDir);
}
try {
execSync("npx cross-env TAURI_BUILD=true next build", { stdio: "inherit" });
} finally {
- if (existsSync(hiddenDir)) {
- renameSync(hiddenDir, apiDir);
+ if (existsSync(hiddenApiDir)) {
+ renameSync(hiddenApiDir, apiDir);
+ }
+ if (existsSync(hiddenAdminDir)) {
+ renameSync(hiddenAdminDir, adminDir);
}
}
diff --git a/scripts/generate-apple-jwt.ts b/scripts/generate-apple-jwt.ts
index fdb971ca..600ec68f 100644
--- a/scripts/generate-apple-jwt.ts
+++ b/scripts/generate-apple-jwt.ts
@@ -35,8 +35,7 @@ export async function generateAppleJWT({
}
// --- CLI Execution Logic ---
-// In TS, we check if this file is the entry point differently depending on your runner
-if (require.main === module || process.argv[1]?.includes("generate-apple-jwt")) {
+if (process.argv[1]?.includes("generate-apple-jwt")) {
const config: AppleAuthConfig = {
teamId: process.env.AUTH_APPLE_TEAM_ID!,
clientId: process.env.AUTH_APPLE_CLIENT_ID!,
diff --git a/launch.sh b/scripts/launch.sh
old mode 100755
new mode 100644
similarity index 100%
rename from launch.sh
rename to scripts/launch.sh
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
new file mode 100644
index 00000000..d486309f
--- /dev/null
+++ b/src/app/admin/layout.tsx
@@ -0,0 +1,15 @@
+import { ReactNode } from "react";
+import { redirect } from "next/navigation";
+import { UserRole } from "../../generated/client/client";
+import { auth } from "@src/auth";
+
+export default async function AdminLayout({ children }: { children: ReactNode }) {
+ const session = await auth();
+ const role = (session?.user as unknown as { role?: UserRole } | undefined)?.role;
+
+ if (!session?.user || role !== UserRole.ADMIN) {
+ redirect("/");
+ }
+
+ return <>{children}>;
+}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
new file mode 100644
index 00000000..56f39e72
--- /dev/null
+++ b/src/app/admin/page.tsx
@@ -0,0 +1,14 @@
+import { auth } from "@src/auth";
+import AdminShell from "@components/admin/AdminShell";
+import StatsCards from "@components/admin/StatsCards";
+
+export default async function AdminHomePage() {
+ const session = await auth();
+ const email = session?.user?.email ?? "";
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/admin/projects/[projectId]/page.tsx b/src/app/admin/projects/[projectId]/page.tsx
new file mode 100644
index 00000000..950d41dd
--- /dev/null
+++ b/src/app/admin/projects/[projectId]/page.tsx
@@ -0,0 +1,16 @@
+import { auth } from "@src/auth";
+import AdminShell from "@components/admin/AdminShell";
+import ProjectDetail from "@components/admin/ProjectDetail";
+
+type Props = { params: Promise<{ projectId: string }> };
+
+export default async function AdminProjectDetailPage({ params }: Props) {
+ const [session, { projectId }] = await Promise.all([auth(), params]);
+ const email = session?.user?.email ?? "";
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/admin/projects/page.tsx b/src/app/admin/projects/page.tsx
new file mode 100644
index 00000000..ce0e40b7
--- /dev/null
+++ b/src/app/admin/projects/page.tsx
@@ -0,0 +1,14 @@
+import { auth } from "@src/auth";
+import AdminShell from "@components/admin/AdminShell";
+import ProjectSearch from "@components/admin/ProjectSearch";
+
+export default async function AdminProjectsPage() {
+ const session = await auth();
+ const email = session?.user?.email ?? "";
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/admin/users/[userId]/page.tsx b/src/app/admin/users/[userId]/page.tsx
new file mode 100644
index 00000000..f2853441
--- /dev/null
+++ b/src/app/admin/users/[userId]/page.tsx
@@ -0,0 +1,17 @@
+import { auth } from "@src/auth";
+import AdminShell from "@components/admin/AdminShell";
+import UserDetail from "@components/admin/UserDetail";
+
+type Props = { params: Promise<{ userId: string }> };
+
+export default async function AdminUserDetailPage({ params }: Props) {
+ const session = await auth();
+ const email = session?.user?.email ?? "";
+ const { userId } = await params;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx
new file mode 100644
index 00000000..92cc9fb7
--- /dev/null
+++ b/src/app/admin/users/page.tsx
@@ -0,0 +1,14 @@
+import { auth } from "@src/auth";
+import AdminShell from "@components/admin/AdminShell";
+import UserSearch from "@components/admin/UserSearch";
+
+export default async function AdminUsersPage() {
+ const session = await auth();
+ const email = session?.user?.email ?? "";
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/api/admin/projects/[projectId]/invites/route.ts b/src/app/api/admin/projects/[projectId]/invites/route.ts
new file mode 100644
index 00000000..850f845e
--- /dev/null
+++ b/src/app/api/admin/projects/[projectId]/invites/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as ProjectService from "@src/server/service/project-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ projectId: z.string().min(1) });
+
+async function getProjectInvites(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { projectId } = validate(ParamsSchema, routeParams);
+
+ const invites = await ProjectService.getInvitesWithMeta(projectId);
+ return Success(invites);
+}
+
+export const GET = apiHandler(getProjectInvites);
diff --git a/src/app/api/admin/projects/[projectId]/members/route.ts b/src/app/api/admin/projects/[projectId]/members/route.ts
new file mode 100644
index 00000000..4c76dd8c
--- /dev/null
+++ b/src/app/api/admin/projects/[projectId]/members/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as ProjectService from "@src/server/service/project-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ projectId: z.string().min(1) });
+
+async function getProjectMembers(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { projectId } = validate(ParamsSchema, routeParams);
+
+ const collaborators = await ProjectService.getCollaborators(projectId);
+ return Success(collaborators);
+}
+
+export const GET = apiHandler(getProjectMembers);
diff --git a/src/app/api/admin/projects/[projectId]/route.ts b/src/app/api/admin/projects/[projectId]/route.ts
new file mode 100644
index 00000000..b0da7c0d
--- /dev/null
+++ b/src/app/api/admin/projects/[projectId]/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as ProjectService from "@src/server/service/project-service";
+import { Success, validate, NotFoundError } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ projectId: z.string().min(1) });
+
+async function getProject(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { projectId } = validate(ParamsSchema, routeParams);
+
+ const project = await ProjectService.getProjectById(projectId);
+ if (!project) throw new NotFoundError();
+
+ return Success(project);
+}
+
+export const GET = apiHandler(getProject);
diff --git a/src/app/api/admin/projects/route.ts b/src/app/api/admin/projects/route.ts
new file mode 100644
index 00000000..003a394f
--- /dev/null
+++ b/src/app/api/admin/projects/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as ProjectService from "@src/server/service/project-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const QuerySchema = z.object({
+ q: z.string().trim().min(1).max(256).optional(),
+ limit: z.coerce.number().int().min(1).max(100).optional(),
+ cursor: z.coerce.number().int().min(0).optional(),
+});
+
+async function searchProjects(req: NextRequest, { searchParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+
+ const { q, limit = 25, cursor } = validate(QuerySchema, searchParams);
+ const term = (q ?? "").trim();
+
+ if (!term) {
+ return Success({ projects: [], nextCursor: null });
+ }
+
+ const projects = await ProjectService.searchProjects(term, limit + 1, cursor);
+ const hasMore = projects.length > limit;
+ const page = hasMore ? projects.slice(0, limit) : projects;
+ const nextCursor = hasMore ? (cursor ?? 0) + limit : null;
+
+ return Success({ projects: page, nextCursor });
+}
+
+export const GET = apiHandler(searchProjects);
diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts
new file mode 100644
index 00000000..2a9e3cf8
--- /dev/null
+++ b/src/app/api/admin/stats/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest } from "next/server";
+
+import * as UserService from "@src/server/service/user-service";
+import * as ProjectService from "@src/server/service/project-service";
+import * as TransactionService from "@src/server/service/transaction-service";
+import { Success } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+async function getStats(req: NextRequest, { user }: AuthApiContext) {
+ await assertAdmin(user);
+
+ const startOfMonth = new Date();
+ startOfMonth.setUTCDate(1);
+ startOfMonth.setUTCHours(0, 0, 0, 0);
+
+ const [userCount, activeProCount, projectCount, transactionsThisMonth] = await Promise.all([
+ UserService.countUsers(),
+ UserService.countActiveProUsers(),
+ ProjectService.countProjects(),
+ TransactionService.countTransactionsSince(startOfMonth),
+ ]);
+
+ return Success({
+ userCount,
+ activeProCount,
+ projectCount,
+ transactionsThisMonth,
+ });
+}
+
+export const GET = apiHandler(getStats);
diff --git a/src/app/api/admin/users/[userId]/projects/route.ts b/src/app/api/admin/users/[userId]/projects/route.ts
new file mode 100644
index 00000000..b78f4297
--- /dev/null
+++ b/src/app/api/admin/users/[userId]/projects/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as ProjectService from "@src/server/service/project-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ userId: z.string().min(1) });
+
+async function getUserProjects(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { userId } = validate(ParamsSchema, routeParams);
+
+ const memberships = await ProjectService.getMemberships(userId);
+ return Success(memberships ?? []);
+}
+
+export const GET = apiHandler(getUserProjects);
diff --git a/src/app/api/admin/users/[userId]/route.ts b/src/app/api/admin/users/[userId]/route.ts
new file mode 100644
index 00000000..8d79a2c7
--- /dev/null
+++ b/src/app/api/admin/users/[userId]/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as UserService from "@src/server/service/user-service";
+import * as ProjectService from "@src/server/service/project-service";
+import * as TransactionService from "@src/server/service/transaction-service";
+import { Success, UserNotFoundError, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ userId: z.string().min(1) });
+
+async function getUserDetail(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { userId } = validate(ParamsSchema, routeParams);
+
+ const target = await UserService.getUserFromId(userId);
+ if (!target) throw new UserNotFoundError();
+
+ const [projectCount, transactionCount] = await Promise.all([
+ ProjectService.countMembershipsByUser(userId),
+ TransactionService.countTransactionsByUser(userId),
+ ]);
+
+ return Success({
+ user: {
+ id: target.id,
+ email: target.email,
+ createdAt: target.createdAt,
+ emailVerified: target.emailVerified,
+ username: target.username,
+ role: target.role,
+ isProUntil: target.isProUntil,
+ isSubscriptionCancelled: target.isSubscriptionCancelled,
+ subscriptionProvider: target.subscriptionProvider,
+ },
+ projectCount,
+ transactionCount,
+ });
+}
+
+export const GET = apiHandler(getUserDetail);
diff --git a/src/app/api/admin/users/[userId]/transactions/route.ts b/src/app/api/admin/users/[userId]/transactions/route.ts
new file mode 100644
index 00000000..a5881d08
--- /dev/null
+++ b/src/app/api/admin/users/[userId]/transactions/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as TransactionService from "@src/server/service/transaction-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const ParamsSchema = z.object({ userId: z.string().min(1) });
+
+async function getUserTransactions(req: NextRequest, { routeParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+ const { userId } = validate(ParamsSchema, routeParams);
+
+ const transactions = await TransactionService.getTransactionsByUser(userId);
+ return Success(transactions);
+}
+
+export const GET = apiHandler(getUserTransactions);
diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts
new file mode 100644
index 00000000..c134b4bb
--- /dev/null
+++ b/src/app/api/admin/users/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest } from "next/server";
+import z from "zod";
+
+import * as UserService from "@src/server/service/user-service";
+import { Success, validate } from "@src/lib/utils/api-utils";
+import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
+import { assertAdmin } from "@src/lib/utils/admin-guard";
+
+const QuerySchema = z.object({
+ q: z.string().trim().min(1).max(256).optional(),
+ limit: z.coerce.number().int().min(1).max(100).optional(),
+ cursor: z.coerce.number().int().min(0).optional(),
+});
+
+async function searchUsers(req: NextRequest, { searchParams, user }: AuthApiContext) {
+ await assertAdmin(user);
+
+ const { q, limit = 25, cursor } = validate(QuerySchema, searchParams);
+ const term = (q ?? "").trim();
+
+ if (!term) {
+ return Success({ users: [], nextCursor: null });
+ }
+
+ const users = await UserService.searchUsers(term, limit + 1, cursor);
+ const hasMore = users.length > limit;
+ const page = hasMore ? users.slice(0, limit) : users;
+ const nextCursor = hasMore ? (cursor ?? 0) + limit : null;
+
+ return Success({ users: page, nextCursor });
+}
+
+export const GET = apiHandler(searchUsers);
diff --git a/src/app/api/metrics/route.ts b/src/app/api/metrics/route.ts
new file mode 100644
index 00000000..17645d93
--- /dev/null
+++ b/src/app/api/metrics/route.ts
@@ -0,0 +1,30 @@
+import { NextRequest, NextResponse } from "next/server";
+import { timingSafeEqual } from "crypto";
+import { registry } from "../../../lib/metrics/registry";
+import { startDbSizeCollector } from "../../../lib/metrics/db-size-collector";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+startDbSizeCollector();
+
+const isAuthorized = (header: string | null): boolean => {
+ const expected = process.env.METRICS_BEARER_TOKEN;
+ if (!expected || !header?.startsWith("Bearer ")) return false;
+ const provided = header.slice("Bearer ".length);
+ const a = new Uint8Array(Buffer.from(provided));
+ const b = new Uint8Array(Buffer.from(expected));
+ if (a.length !== b.length) return false;
+ return timingSafeEqual(a, b);
+};
+
+export const GET = async (req: NextRequest) => {
+ if (!isAuthorized(req.headers.get("authorization"))) {
+ return new NextResponse("Unauthorized", { status: 401 });
+ }
+ const body = await registry.metrics();
+ return new NextResponse(body, {
+ status: 200,
+ headers: { "Content-Type": registry.contentType },
+ });
+};
diff --git a/src/app/api/projects/[projectId]/invite/route.ts b/src/app/api/projects/[projectId]/invite/route.ts
index 4879bb01..c6f3a99c 100644
--- a/src/app/api/projects/[projectId]/invite/route.ts
+++ b/src/app/api/projects/[projectId]/invite/route.ts
@@ -1,5 +1,5 @@
import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../../generated/client/client";
import {
ForbiddenError,
InternalServerError,
diff --git a/src/app/api/projects/[projectId]/members/[userId]/route.ts b/src/app/api/projects/[projectId]/members/[userId]/route.ts
index 8a89ae1f..3ad9c65d 100644
--- a/src/app/api/projects/[projectId]/members/[userId]/route.ts
+++ b/src/app/api/projects/[projectId]/members/[userId]/route.ts
@@ -1,4 +1,4 @@
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../../../generated/client/client";
import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
import {
ForbiddenError,
@@ -12,7 +12,7 @@ import {
import * as Roles from "@src/lib/utils/roles";
import * as ProjectService from "@src/server/service/project-service";
-import * as CollabUtils from "@src/lib/collaboration/utils";
+import * as CollabUtils from "@src/lib/cloud/utils";
import z from "zod";
import { NextRequest } from "next/server";
diff --git a/src/app/api/projects/[projectId]/route.ts b/src/app/api/projects/[projectId]/route.ts
index 9e39e2da..e88c9ac3 100644
--- a/src/app/api/projects/[projectId]/route.ts
+++ b/src/app/api/projects/[projectId]/route.ts
@@ -1,4 +1,4 @@
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../generated/client/client";
import * as S3 from "@src/lib/s3";
import * as ProjectService from "@src/server/service/project-service";
diff --git a/src/app/api/projects/[projectId]/saves/manual/route.ts b/src/app/api/projects/[projectId]/saves/manual/route.ts
index ad318231..03628455 100644
--- a/src/app/api/projects/[projectId]/saves/manual/route.ts
+++ b/src/app/api/projects/[projectId]/saves/manual/route.ts
@@ -1,4 +1,4 @@
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../../../generated/client/client";
import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
import {
ForbiddenError,
diff --git a/src/app/api/projects/[projectId]/saves/restore/route.ts b/src/app/api/projects/[projectId]/saves/restore/route.ts
index fcc4338e..baa5e054 100644
--- a/src/app/api/projects/[projectId]/saves/restore/route.ts
+++ b/src/app/api/projects/[projectId]/saves/restore/route.ts
@@ -1,4 +1,4 @@
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../../../generated/client/client";
import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
import { ForbiddenError, getCollabHttpUrl, Success, validate } from "@src/lib/utils/api-utils";
diff --git a/src/app/api/projects/[projectId]/saves/route.ts b/src/app/api/projects/[projectId]/saves/route.ts
index 99ca56e1..b329c024 100644
--- a/src/app/api/projects/[projectId]/saves/route.ts
+++ b/src/app/api/projects/[projectId]/saves/route.ts
@@ -1,4 +1,4 @@
-import { ProjectRole } from "@prisma/client";
+import { ProjectRole } from "../../../../../generated/client/client";
import { apiHandler, AuthApiContext } from "@src/lib/utils/api-handler";
import { ForbiddenError, getCollabHttpUrl, Success, validate } from "@src/lib/utils/api-utils";
diff --git a/src/auth.ts b/src/auth.ts
index 4510fcdb..d0bd650d 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -2,6 +2,7 @@ import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Apple from "next-auth/providers/apple";
import { PrismaAdapter } from "@auth/prisma-adapter";
+import { UserRole } from "./generated/client/client";
import prisma from "@src/server/db";
import * as UserService from "@src/server/service/user-service";
@@ -26,13 +27,19 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
if (user) {
token.id = user.id;
token.email = user.email;
- // `createdAt` is set on programmatic sign-in (string) and missing for OAuth — fill it in
+ // `createdAt` may be available on programmatic sign-in but not OAuth.
const extUser = user as typeof user & { createdAt?: string };
if (extUser.createdAt) {
token.createdAt = extUser.createdAt;
- } else if (token.id && !token.createdAt) {
- const dbUser = await UserService.getUserFromId(token.id as string);
- if (dbUser) token.createdAt = dbUser.createdAt.toISOString();
+ }
+ }
+ // Always sync from DB so role changes (e.g. granting admin) are reflected
+ // without requiring a sign-out. Called on every auth() invocation.
+ if (token.id) {
+ const dbUser = await UserService.getUserFromId(token.id as string);
+ if (dbUser) {
+ if (!token.createdAt) token.createdAt = dbUser.createdAt.toISOString();
+ token.role = dbUser.role;
}
}
return token;
@@ -44,7 +51,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
id: token.id as string,
email: token.email as string,
createdAt: token.createdAt ? new Date(token.createdAt as string) : new Date(),
- } as typeof session.user & { id: string; email: string; createdAt: Date };
+ role: (token.role as UserRole) ?? UserRole.USER,
+ } as typeof session.user & {
+ id: string;
+ email: string;
+ createdAt: Date;
+ role: UserRole;
+ };
}
return session;
},
diff --git a/src/context/ProjectContext.tsx b/src/context/ProjectContext.tsx
index c1e2144e..84511436 100644
--- a/src/context/ProjectContext.tsx
+++ b/src/context/ProjectContext.tsx
@@ -1,6 +1,14 @@
"use client";
-import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
import { Editor } from "@tiptap/react";
import { CharacterMap, mergeCharactersData } from "@src/lib/screenplay/characters";
import { LocationMap, mergeLocationsData } from "@src/lib/screenplay/locations";
@@ -22,7 +30,7 @@ import { ScreenplayElement, TitlePageElement, Style, PageFormat } from "@src/lib
import { SearchMatch } from "@src/lib/screenplay/extensions/search-highlight-extension";
// Import types only - these don't cause module loading
-import type { ThrottledWebsocketProvider } from "@src/lib/collaboration/utils";
+import type { ThrottledWebsocketProvider } from "@src/lib/cloud/utils";
import type { ProjectRepository } from "@src/lib/project/project-repository";
// -------------------------------- //
@@ -258,7 +266,9 @@ export const ProjectProvider = ({ children, projectId }: ProjectProviderProps) =
const [characters, setCharacters] = useState(undefined);
const [locations, setLocations] = useState(undefined);
const [scenes, setScenes] = useState([]);
- const [selectedElement, setSelectedElementState] = useState(ScreenplayElement.Action);
+ const [selectedElement, setSelectedElementState] = useState(
+ ScreenplayElement.Action,
+ );
const [selectedStyles, setSelectedStylesState] = useState