-
- Ações perigosas
-
+
Ações perigosas
Você pode zerar os dados do OpenMonetis e manter seu acesso,
ou excluir sua conta inteira de forma irreversível.
diff --git a/src/features/settings/components/diagnostics-tab.tsx b/src/features/settings/components/diagnostics-tab.tsx
new file mode 100644
index 0000000..d62a4d9
--- /dev/null
+++ b/src/features/settings/components/diagnostics-tab.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import {
+ RiCheckLine,
+ RiCloseCircleLine,
+ RiFileCopyLine,
+ RiWifiLine,
+} from "@remixicon/react";
+import { useState } from "react";
+import { Badge } from "@/shared/components/ui/badge";
+import { Separator } from "@/shared/components/ui/separator";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/shared/components/ui/tooltip";
+import type { DiagnosticsData } from "../diagnostics-queries";
+
+function CopyButton({ value }: { value: string }) {
+ const [copied, setCopied] = useState(false);
+
+ function handleCopy() {
+ navigator.clipboard.writeText(value);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+
+ return (
+
+
+
+
+ {copied ? "Copiado!" : "Copiar"}
+
+ );
+}
+
+function Row({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {label}
+
+ {children}
+
+
+ );
+}
+
+function Section({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+
+
{children}
+
+ );
+}
+
+function StatusBadge({ ok, labelOk = "Configurado", labelFail = "Não configurado" }: { ok: boolean; labelOk?: string; labelFail?: string }) {
+ return ok ? (
+
+
+ {labelOk}
+
+ ) : (
+
+
+ {labelFail}
+
+ );
+}
+
+function formatDate(date: Date) {
+ return new Intl.DateTimeFormat("pt-BR", {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(new Date(date));
+}
+
+export function DiagnosticsTab({ data }: { data: DiagnosticsData }) {
+ return (
+
+
+
+ {data.identity.userId}
+
+
+ {data.identity.name}
+ {data.identity.email}
+
+
+
+
+
+ {formatDate(data.session.createdAt)}
+ {formatDate(data.session.expiresAt)}
+
+
+
+
+
+
+ v{data.app.version}
+
+
+
+ {data.app.nodeEnv}
+
+
+ {data.app.buildSha && (
+
+ {data.app.buildSha.slice(0, 8)}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {data.server.publicDomain ? (
+ {data.server.publicDomain}
+ ) : (
+ Não definido
+ )}
+
+
+
+
+
+
+
+ {data.health.db === "ok" ? (
+
+
+ Online
+ {data.health.dbLatencyMs !== null && (
+
+ · {data.health.dbLatencyMs}ms
+
+ )}
+
+ ) : (
+
+
+ Erro
+
+ )}
+
+
+
+
+
+
+
+ {data.usage.transactions.toLocaleString("pt-BR")}
+
+
+ {data.usage.attachments.toLocaleString("pt-BR")}
+
+
+ {data.usage.notes.toLocaleString("pt-BR")}
+
+
+ {data.usage.inboxItems.toLocaleString("pt-BR")}
+
+
+
+ );
+}
diff --git a/src/features/settings/diagnostics-queries.ts b/src/features/settings/diagnostics-queries.ts
new file mode 100644
index 0000000..3faa0a5
--- /dev/null
+++ b/src/features/settings/diagnostics-queries.ts
@@ -0,0 +1,96 @@
+import { count, eq } from "drizzle-orm";
+import {
+ attachments,
+ inboxItems,
+ notes,
+ transactions,
+} from "@/db/schema";
+import { db } from "@/shared/lib/db";
+
+export type DiagnosticsData = {
+ identity: {
+ userId: string;
+ name: string;
+ email: string;
+ };
+ session: {
+ createdAt: Date;
+ expiresAt: Date;
+ };
+ app: {
+ version: string;
+ nodeEnv: string;
+ buildSha: string | null;
+ };
+ server: {
+ s3Configured: boolean;
+ emailConfigured: boolean;
+ publicDomain: string | null;
+ };
+ health: {
+ db: "ok" | "error";
+ dbLatencyMs: number | null;
+ };
+ usage: {
+ transactions: number;
+ attachments: number;
+ notes: number;
+ inboxItems: number;
+ };
+};
+
+export async function fetchDiagnosticsData(
+ userId: string,
+ user: { id: string; name: string; email: string },
+ session: { createdAt: Date; expiresAt: Date },
+): Promise
{
+ const dbStart = Date.now();
+ let dbStatus: "ok" | "error" = "ok";
+ let dbLatencyMs: number | null = null;
+
+ try {
+ await db.execute("SELECT 1");
+ dbLatencyMs = Date.now() - dbStart;
+ } catch {
+ dbStatus = "error";
+ }
+
+ const [txCount, attachCount, notesCount, inboxCount] = await Promise.all([
+ db.select({ value: count() }).from(transactions).where(eq(transactions.userId, userId)),
+ db.select({ value: count() }).from(attachments).where(eq(attachments.userId, userId)),
+ db.select({ value: count() }).from(notes).where(eq(notes.userId, userId)),
+ db.select({ value: count() }).from(inboxItems).where(eq(inboxItems.userId, userId)),
+ ]);
+
+ return {
+ identity: {
+ userId: user.id,
+ name: user.name,
+ email: user.email,
+ },
+ session: {
+ createdAt: session.createdAt,
+ expiresAt: session.expiresAt,
+ },
+ app: {
+ version: process.env.npm_package_version ?? "—",
+ nodeEnv: process.env.NODE_ENV ?? "—",
+ buildSha: process.env.BUILD_SHA ?? null,
+ },
+ server: {
+ s3Configured: !!(process.env.S3_ENDPOINT && process.env.S3_ACCESS_KEY_ID),
+ emailConfigured: !!process.env.RESEND_FROM_EMAIL,
+ publicDomain: process.env.PUBLIC_DOMAIN ?? null,
+ },
+ health: {
+ db: dbStatus,
+ dbLatencyMs,
+ },
+ usage: {
+ transactions: txCount[0]?.value ?? 0,
+ attachments: attachCount[0]?.value ?? 0,
+ notes: notesCount[0]?.value ?? 0,
+ inboxItems: inboxCount[0]?.value ?? 0,
+ },
+ };
+}
diff --git a/src/shared/components/navigation/navbar/navbar-user.tsx b/src/shared/components/navigation/navbar/navbar-user.tsx
index cbfd8a4..47cfd1d 100644
--- a/src/shared/components/navigation/navbar/navbar-user.tsx
+++ b/src/shared/components/navigation/navbar/navbar-user.tsx
@@ -1,6 +1,8 @@
"use client";
import {
+ RiCheckLine,
+ RiFileCopyLine,
RiHistoryLine,
RiLogoutCircleLine,
RiMegaphoneLine,
@@ -15,6 +17,11 @@ import { version } from "@/package.json";
import { FeedbackDialogBody } from "@/shared/components/navigation/navbar/feedback-dialog";
import { Badge } from "@/shared/components/ui/badge";
import { Dialog, DialogTrigger } from "@/shared/components/ui/dialog";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/shared/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
@@ -50,6 +57,13 @@ export function NavbarUser({
const router = useRouter();
const [logoutLoading, setLogoutLoading] = useState(false);
const [feedbackOpen, setFeedbackOpen] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ function handleCopyId() {
+ navigator.clipboard.writeText(user.id);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
const avatarSrc = pagadorAvatarUrl
? getAvatarSrc(pagadorAvatarUrl)
@@ -106,9 +120,30 @@ export function NavbarUser({