feat: update credential editor to use submitting system and add health monitor
This commit is contained in:
90
src/lib/db-health-monitor.ts
Normal file
90
src/lib/db-health-monitor.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
type EventListener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
class DatabaseHealthMonitor {
|
||||||
|
private static instance: DatabaseHealthMonitor;
|
||||||
|
private dbHealthy: boolean = true;
|
||||||
|
private lastCheckTime: number = 0;
|
||||||
|
private checkInProgress: boolean = false;
|
||||||
|
private listeners: Map<string, EventListener[]> = new Map();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): DatabaseHealthMonitor {
|
||||||
|
if (!DatabaseHealthMonitor.instance) {
|
||||||
|
DatabaseHealthMonitor.instance = new DatabaseHealthMonitor();
|
||||||
|
}
|
||||||
|
return DatabaseHealthMonitor.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: string, listener: EventListener): void {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(event)!.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, listener: EventListener): void {
|
||||||
|
const eventListeners = this.listeners.get(event);
|
||||||
|
if (eventListeners) {
|
||||||
|
const index = eventListeners.indexOf(listener);
|
||||||
|
if (index !== -1) {
|
||||||
|
eventListeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(event: string, ...args: any[]): void {
|
||||||
|
const eventListeners = this.listeners.get(event);
|
||||||
|
if (eventListeners) {
|
||||||
|
eventListeners.forEach((listener) => listener(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportDatabaseError(error: any) {
|
||||||
|
const errorMessage = error?.response?.data?.error || error?.message || "";
|
||||||
|
const errorCode = error?.response?.data?.code || error?.code;
|
||||||
|
|
||||||
|
const isDatabaseError =
|
||||||
|
errorMessage.toLowerCase().includes("database") ||
|
||||||
|
errorMessage.toLowerCase().includes("sqlite") ||
|
||||||
|
errorMessage.toLowerCase().includes("drizzle") ||
|
||||||
|
errorCode === "DATABASE_ERROR" ||
|
||||||
|
errorCode === "DB_CONNECTION_FAILED";
|
||||||
|
|
||||||
|
const isBackendUnreachable =
|
||||||
|
errorCode === "ERR_NETWORK" ||
|
||||||
|
errorCode === "ECONNREFUSED" ||
|
||||||
|
(errorMessage.toLowerCase().includes("network error") &&
|
||||||
|
error?.response === undefined);
|
||||||
|
|
||||||
|
if ((isDatabaseError || isBackendUnreachable) && this.dbHealthy) {
|
||||||
|
this.dbHealthy = false;
|
||||||
|
this.emit("database-connection-lost", {
|
||||||
|
error: errorMessage || "Backend server unreachable",
|
||||||
|
code: errorCode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportDatabaseSuccess() {
|
||||||
|
if (!this.dbHealthy) {
|
||||||
|
this.dbHealthy = true;
|
||||||
|
this.emit("database-connection-restored", {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDatabaseHealthy(): boolean {
|
||||||
|
return this.dbHealthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.dbHealthy = true;
|
||||||
|
this.lastCheckTime = 0;
|
||||||
|
this.checkInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dbHealthMonitor = DatabaseHealthMonitor.getInstance();
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
"passwordRequired": "Password is required",
|
"passwordRequired": "Password is required",
|
||||||
"sshKeyRequired": "SSH key is required",
|
"sshKeyRequired": "SSH key is required",
|
||||||
"credentialAddedSuccessfully": "Credential \"{{name}}\" added successfully",
|
"credentialAddedSuccessfully": "Credential \"{{name}}\" added successfully",
|
||||||
|
"savingCredential": "Saving credential...",
|
||||||
|
"updatingCredential": "Updating credential...",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { toast } from "sonner";
|
|||||||
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
||||||
import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts";
|
import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts";
|
||||||
import { useTheme } from "@/components/theme-provider";
|
import { useTheme } from "@/components/theme-provider";
|
||||||
|
import { dbHealthMonitor } from "@/lib/db-health-monitor.ts";
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
@@ -36,6 +37,7 @@ function AppContent() {
|
|||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const [rightSidebarOpen, setRightSidebarOpen] = useState(false);
|
const [rightSidebarOpen, setRightSidebarOpen] = useState(false);
|
||||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(400);
|
const [rightSidebarWidth, setRightSidebarWidth] = useState(400);
|
||||||
|
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
|
||||||
|
|
||||||
const isDarkMode =
|
const isDarkMode =
|
||||||
theme === "dark" ||
|
theme === "dark" ||
|
||||||
@@ -47,6 +49,38 @@ function AppContent() {
|
|||||||
|
|
||||||
const lastAltPressTime = useRef(0);
|
const lastAltPressTime = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDatabaseConnectionLost = () => {
|
||||||
|
setDbConnectionFailed(true);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDatabaseConnectionRestored = () => {
|
||||||
|
setDbConnectionFailed(false);
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
dbHealthMonitor.on(
|
||||||
|
"database-connection-lost",
|
||||||
|
handleDatabaseConnectionLost,
|
||||||
|
);
|
||||||
|
dbHealthMonitor.on(
|
||||||
|
"database-connection-restored",
|
||||||
|
handleDatabaseConnectionRestored,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dbHealthMonitor.off(
|
||||||
|
"database-connection-lost",
|
||||||
|
handleDatabaseConnectionLost,
|
||||||
|
);
|
||||||
|
dbHealthMonitor.off(
|
||||||
|
"database-connection-restored",
|
||||||
|
handleDatabaseConnectionRestored,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.code === "ShiftLeft") {
|
if (event.code === "ShiftLeft") {
|
||||||
@@ -99,7 +133,8 @@ function AppContent() {
|
|||||||
if (hostIdentifier) {
|
if (hostIdentifier) {
|
||||||
const openTerminal = async () => {
|
const openTerminal = async () => {
|
||||||
try {
|
try {
|
||||||
const { getSSHHostById, getSSHHosts } = await import("@/ui/main-axios.ts");
|
const { getSSHHostById, getSSHHosts } =
|
||||||
|
await import("@/ui/main-axios.ts");
|
||||||
let host = null;
|
let host = null;
|
||||||
|
|
||||||
// Pure numeric → lookup by ID
|
// Pure numeric → lookup by ID
|
||||||
@@ -108,7 +143,9 @@ function AppContent() {
|
|||||||
} else {
|
} else {
|
||||||
// Non-numeric → lookup by name (first match)
|
// Non-numeric → lookup by name (first match)
|
||||||
const hosts = await getSSHHosts();
|
const hosts = await getSSHHosts();
|
||||||
host = hosts.find((h: { name?: string }) => h.name === hostIdentifier) || null;
|
host =
|
||||||
|
hosts.find((h: { name?: string }) => h.name === hostIdentifier) ||
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host) {
|
if (host) {
|
||||||
@@ -232,10 +269,10 @@ function AppContent() {
|
|||||||
const showProfile = currentTabData?.type === "user_profile";
|
const showProfile = currentTabData?.type === "user_profile";
|
||||||
const showNetworkGraph = currentTabData?.type === "network_graph";
|
const showNetworkGraph = currentTabData?.type === "network_graph";
|
||||||
|
|
||||||
if (authLoading) {
|
if (authLoading && !dbConnectionFailed) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen w-screen flex items-center justify-center"
|
className="fixed inset-0 flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-elevated)",
|
background: "var(--bg-elevated)",
|
||||||
backgroundImage: `repeating-linear-gradient(
|
backgroundImage: `repeating-linear-gradient(
|
||||||
@@ -247,13 +284,39 @@ function AppContent() {
|
|||||||
)`,
|
)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
|
||||||
<div className="w-16 h-16 border-4 border-primary/30 border-t-primary rounded-full animate-spin mx-auto" />
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<div className="w-16 h-16 border-4 border-primary/30 border-t-primary rounded-full animate-spin mx-auto" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbConnectionFailed) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-screen overflow-hidden bg-background">
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-[10000] bg-background">
|
||||||
|
<Dashboard
|
||||||
|
isAuthenticated={false}
|
||||||
|
authLoading={false}
|
||||||
|
onAuthSuccess={handleAuthSuccess}
|
||||||
|
isTopbarOpen={isTopbarOpen}
|
||||||
|
onSelectView={() => {}}
|
||||||
|
initialDbError="Database connection failed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
richColors={false}
|
||||||
|
closeButton
|
||||||
|
duration={5000}
|
||||||
|
offset={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen overflow-hidden bg-background">
|
<div className="h-screen w-screen overflow-hidden bg-background">
|
||||||
<CommandPalette
|
<CommandPalette
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ interface DashboardProps {
|
|||||||
isTopbarOpen: boolean;
|
isTopbarOpen: boolean;
|
||||||
rightSidebarOpen?: boolean;
|
rightSidebarOpen?: boolean;
|
||||||
rightSidebarWidth?: number;
|
rightSidebarWidth?: number;
|
||||||
|
initialDbError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard({
|
export function Dashboard({
|
||||||
@@ -66,13 +67,14 @@ export function Dashboard({
|
|||||||
isTopbarOpen,
|
isTopbarOpen,
|
||||||
rightSidebarOpen = false,
|
rightSidebarOpen = false,
|
||||||
rightSidebarWidth = 400,
|
rightSidebarWidth = 400,
|
||||||
|
initialDbError = null,
|
||||||
}: DashboardProps): React.ReactElement {
|
}: DashboardProps): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [, setUsername] = useState<string | null>(null);
|
const [, setUsername] = useState<string | null>(null);
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
const [dbError, setDbError] = useState<string | null>(null);
|
const [dbError, setDbError] = useState<string | null>(initialDbError);
|
||||||
|
|
||||||
const [uptime, setUptime] = useState<string>("0d 0h 0m");
|
const [uptime, setUptime] = useState<string>("0d 0h 0m");
|
||||||
const [versionStatus, setVersionStatus] = useState<
|
const [versionStatus, setVersionStatus] = useState<
|
||||||
@@ -160,7 +162,8 @@ export function Dashboard({
|
|||||||
const uptimeInfo = await getUptime();
|
const uptimeInfo = await getUptime();
|
||||||
setUptime(uptimeInfo.formatted);
|
setUptime(uptimeInfo.formatted);
|
||||||
|
|
||||||
const updateCheckDisabled = localStorage.getItem("disableUpdateCheck") === "true";
|
const updateCheckDisabled =
|
||||||
|
localStorage.getItem("disableUpdateCheck") === "true";
|
||||||
if (!updateCheckDisabled) {
|
if (!updateCheckDisabled) {
|
||||||
const versionInfo = await getVersionInfo();
|
const versionInfo = await getVersionInfo();
|
||||||
setVersionText(`v${versionInfo.localVersion}`);
|
setVersionText(`v${versionInfo.localVersion}`);
|
||||||
@@ -606,7 +609,9 @@ export function Dashboard({
|
|||||||
{showNetworkGraph ? (
|
{showNetworkGraph ? (
|
||||||
<>
|
<>
|
||||||
<Network className="mr-3" />
|
<Network className="mr-3" />
|
||||||
{t("dashboard.networkGraph", { defaultValue: "Network Graph" })}
|
{t("dashboard.networkGraph", {
|
||||||
|
defaultValue: "Network Graph",
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -700,10 +705,7 @@ export function Dashboard({
|
|||||||
>
|
>
|
||||||
{recentActivityLoading ? (
|
{recentActivityLoading ? (
|
||||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
||||||
<Loader2
|
<Loader2 className="animate-spin mr-2" size={16} />
|
||||||
className="animate-spin mr-2"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
<span>{t("dashboard.loadingRecentActivity")}</span>
|
<span>{t("dashboard.loadingRecentActivity")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : recentActivity.length === 0 ? (
|
) : recentActivity.length === 0 ? (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type {
|
|||||||
} from "../../../../../types";
|
} from "../../../../../types";
|
||||||
import { CredentialGeneralTab } from "./tabs/CredentialGeneralTab";
|
import { CredentialGeneralTab } from "./tabs/CredentialGeneralTab";
|
||||||
import { CredentialAuthenticationTab } from "./tabs/CredentialAuthenticationTab";
|
import { CredentialAuthenticationTab } from "./tabs/CredentialAuthenticationTab";
|
||||||
|
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||||
|
|
||||||
export function CredentialEditor({
|
export function CredentialEditor({
|
||||||
editingCredential,
|
editingCredential,
|
||||||
@@ -59,6 +60,7 @@ export function CredentialEditor({
|
|||||||
const keyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const keyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState("general");
|
const [activeTab, setActiveTab] = useState("general");
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<
|
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<
|
||||||
string | null
|
string | null
|
||||||
@@ -168,6 +170,7 @@ export function CredentialEditor({
|
|||||||
resolver: zodResolver(formSchema) as unknown as Parameters<
|
resolver: zodResolver(formSchema) as unknown as Parameters<
|
||||||
typeof useForm<FormData>
|
typeof useForm<FormData>
|
||||||
>[0]["resolver"],
|
>[0]["resolver"],
|
||||||
|
mode: "all",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -183,6 +186,45 @@ export function CredentialEditor({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watchedFields = form.watch();
|
||||||
|
|
||||||
|
const isFormValid = React.useMemo(() => {
|
||||||
|
const values = form.getValues();
|
||||||
|
|
||||||
|
if (!values.name || !values.username) return false;
|
||||||
|
|
||||||
|
if (authTab === "password") {
|
||||||
|
return !!(values.password && values.password.trim() !== "");
|
||||||
|
} else if (authTab === "key") {
|
||||||
|
if (editingCredential) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!values.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, [watchedFields, authTab, editingCredential]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateAuthFields = async () => {
|
||||||
|
form.setValue("authType", authTab, { shouldValidate: true });
|
||||||
|
|
||||||
|
if (authTab === "password") {
|
||||||
|
form.setValue("key", null, { shouldValidate: true });
|
||||||
|
form.setValue("publicKey", "", { shouldValidate: true });
|
||||||
|
form.setValue("keyPassword", "", { shouldValidate: true });
|
||||||
|
form.setValue("keyType", "auto", { shouldValidate: true });
|
||||||
|
} else if (authTab === "key") {
|
||||||
|
form.setValue("password", "", { shouldValidate: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await form.trigger();
|
||||||
|
};
|
||||||
|
|
||||||
|
updateAuthFields();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [authTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingCredential && fullCredentialDetails) {
|
if (editingCredential && fullCredentialDetails) {
|
||||||
const defaultAuthType = fullCredentialDetails.authType;
|
const defaultAuthType = fullCredentialDetails.authType;
|
||||||
@@ -331,6 +373,7 @@ export function CredentialEditor({
|
|||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
|
||||||
if (!data.name || data.name.trim() === "") {
|
if (!data.name || data.name.trim() === "") {
|
||||||
@@ -388,6 +431,8 @@ export function CredentialEditor({
|
|||||||
} else {
|
} else {
|
||||||
toast.error(t("credentials.failedToSaveCredential"));
|
toast.error(t("credentials.failedToSaveCredential"));
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -457,9 +502,19 @@ export function CredentialEditor({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-1 flex flex-col h-full min-h-0 w-full"
|
className="flex-1 flex flex-col h-full min-h-0 w-full relative"
|
||||||
key={editingCredential?.id || "new"}
|
key={editingCredential?.id || "new"}
|
||||||
>
|
>
|
||||||
|
<SimpleLoader
|
||||||
|
visible={isSubmitting}
|
||||||
|
message={
|
||||||
|
editingCredential
|
||||||
|
? t("credentials.updatingCredential")
|
||||||
|
: t("credentials.savingCredential")
|
||||||
|
}
|
||||||
|
backgroundColor="var(--bg-base)"
|
||||||
|
/>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit, handleFormError)}
|
onSubmit={form.handleSubmit(onSubmit, handleFormError)}
|
||||||
@@ -529,7 +584,12 @@ export function CredentialEditor({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<footer className="shrink-0 w-full pb-0">
|
<footer className="shrink-0 w-full pb-0">
|
||||||
<Separator className="p-0.25" />
|
<Separator className="p-0.25" />
|
||||||
<Button className="translate-y-2" type="submit" variant="outline">
|
<Button
|
||||||
|
className="translate-y-2"
|
||||||
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!isFormValid || isSubmitting}
|
||||||
|
>
|
||||||
{editingCredential
|
{editingCredential
|
||||||
? t("credentials.updateCredential")
|
? t("credentials.updateCredential")
|
||||||
: t("credentials.addCredential")}
|
: t("credentials.addCredential")}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import axios, { AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios";
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
type AxiosInstance,
|
||||||
|
type AxiosRequestConfig,
|
||||||
|
type AxiosResponse,
|
||||||
|
} from "axios";
|
||||||
import type {
|
import type {
|
||||||
SSHHost,
|
SSHHost,
|
||||||
SSHHostData,
|
SSHHostData,
|
||||||
@@ -63,6 +68,7 @@ import {
|
|||||||
dashboardLogger,
|
dashboardLogger,
|
||||||
type LogContext,
|
type LogContext,
|
||||||
} from "../lib/frontend-logger.js";
|
} from "../lib/frontend-logger.js";
|
||||||
|
import { dbHealthMonitor } from "../lib/db-health-monitor.js";
|
||||||
|
|
||||||
interface FileManagerOperation {
|
interface FileManagerOperation {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -373,13 +379,17 @@ function createApiInstance(
|
|||||||
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbHealthMonitor.reportDatabaseSuccess();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error: AxiosErrorExtended) => {
|
(error: AxiosErrorExtended) => {
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
const startTime = error.config?.startTime;
|
const startTime = error.config?.startTime;
|
||||||
const requestId = error.config?.requestId;
|
const requestId = error.config?.requestId;
|
||||||
const responseTime = startTime ? Math.round(endTime - startTime) : undefined;
|
const responseTime = startTime
|
||||||
|
? Math.round(endTime - startTime)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const method = error.config?.method?.toUpperCase() || "UNKNOWN";
|
const method = error.config?.method?.toUpperCase() || "UNKNOWN";
|
||||||
const url = error.config?.url || "UNKNOWN";
|
const url = error.config?.url || "UNKNOWN";
|
||||||
@@ -422,6 +432,8 @@ function createApiInstance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbHealthMonitor.reportDatabaseError(error);
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
const errorCode = (error.response?.data as Record<string, unknown>)
|
const errorCode = (error.response?.data as Record<string, unknown>)
|
||||||
?.code;
|
?.code;
|
||||||
|
|||||||
Reference in New Issue
Block a user