fix: File cleanup
This commit is contained in:
@@ -153,7 +153,6 @@ export function AdminSettings({
|
||||
toast.error(t("admin.failedToFetchOidcConfig"));
|
||||
}
|
||||
});
|
||||
// Capture the current session so we know whether to ask for a password later.
|
||||
getUserInfo()
|
||||
.then((info) => {
|
||||
if (info) {
|
||||
@@ -251,9 +250,7 @@ export function AdminSettings({
|
||||
};
|
||||
|
||||
const handleTogglePasswordLogin = async (checked: boolean) => {
|
||||
// If disabling password login, warn the user
|
||||
if (!checked) {
|
||||
// Check if OIDC is configured
|
||||
const hasOIDCConfigured =
|
||||
oidcConfig.client_id &&
|
||||
oidcConfig.client_secret &&
|
||||
@@ -276,7 +273,6 @@ export function AdminSettings({
|
||||
await updatePasswordLoginAllowed(checked);
|
||||
setAllowPasswordLogin(checked);
|
||||
|
||||
// Auto-disable registration when password login is disabled
|
||||
if (allowRegistration) {
|
||||
await updateRegistrationAllowed(false);
|
||||
setAllowRegistration(false);
|
||||
@@ -295,7 +291,6 @@ export function AdminSettings({
|
||||
return;
|
||||
}
|
||||
|
||||
// Enabling password login - proceed normally
|
||||
setPasswordLoginLoading(true);
|
||||
try {
|
||||
await updatePasswordLoginAllowed(checked);
|
||||
@@ -493,7 +488,6 @@ export function AdminSettings({
|
||||
const formData = new FormData();
|
||||
formData.append("file", importFile);
|
||||
if (requiresImportPassword) {
|
||||
// Preserve the existing password flow for non-OIDC accounts.
|
||||
formData.append("password", importPassword);
|
||||
}
|
||||
|
||||
@@ -607,7 +601,6 @@ export function AdminSettings({
|
||||
};
|
||||
|
||||
const handleRevokeSession = async (sessionId: string) => {
|
||||
// Check if this is the current session
|
||||
const currentJWT = getCookie("jwt");
|
||||
const currentSession = sessions.find((s) => s.jwtToken === currentJWT);
|
||||
const isCurrentSession = currentSession?.id === sessionId;
|
||||
@@ -641,7 +634,6 @@ export function AdminSettings({
|
||||
if (response.ok) {
|
||||
toast.success(t("admin.sessionRevokedSuccessfully"));
|
||||
|
||||
// If user revoked their own session, reload the page after a brief delay
|
||||
if (isCurrentSession) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
@@ -661,7 +653,6 @@ export function AdminSettings({
|
||||
};
|
||||
|
||||
const handleRevokeAllUserSessions = async (userId: string) => {
|
||||
// Check if revoking sessions for current user
|
||||
const isCurrentUser = currentUser?.id === userId;
|
||||
|
||||
confirmWithToast(
|
||||
@@ -701,7 +692,6 @@ export function AdminSettings({
|
||||
data.message || t("admin.sessionsRevokedSuccessfully"),
|
||||
);
|
||||
|
||||
// If revoking sessions for current user, reload the page after a brief delay
|
||||
if (isCurrentUser) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
@@ -978,7 +968,6 @@ export function AdminSettings({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
// Check if password login is enabled
|
||||
if (!allowPasswordLogin) {
|
||||
confirmWithToast(
|
||||
t("admin.confirmDisableOIDCWarning"),
|
||||
@@ -1469,7 +1458,6 @@ export function AdminSettings({
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Only render the password field when a local account is performing the import. */}
|
||||
{importFile && requiresImportPassword && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="import-password">Password</Label>
|
||||
|
||||
@@ -80,7 +80,6 @@ export function CredentialEditor({
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
} catch {
|
||||
// Failed to load credentials
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ export function Dashboard({
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
|
||||
// Dashboard data state
|
||||
const [uptime, setUptime] = useState<string>("0d 0h 0m");
|
||||
const [versionStatus, setVersionStatus] = useState<
|
||||
"up_to_date" | "requires_update"
|
||||
@@ -141,22 +140,18 @@ export function Dashboard({
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Fetch dashboard data
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
// Fetch uptime
|
||||
const uptimeInfo = await getUptime();
|
||||
setUptime(uptimeInfo.formatted);
|
||||
|
||||
// Fetch version info
|
||||
const versionInfo = await getVersionInfo();
|
||||
setVersionText(`v${versionInfo.localVersion}`);
|
||||
setVersionStatus(versionInfo.status || "up_to_date");
|
||||
|
||||
// Fetch database health
|
||||
try {
|
||||
await getDatabaseHealth();
|
||||
setDbHealth("healthy");
|
||||
@@ -164,25 +159,20 @@ export function Dashboard({
|
||||
setDbHealth("error");
|
||||
}
|
||||
|
||||
// Fetch total counts
|
||||
const hosts = await getSSHHosts();
|
||||
setTotalServers(hosts.length);
|
||||
|
||||
// Count total tunnels across all hosts
|
||||
let totalTunnelsCount = 0;
|
||||
for (const host of hosts) {
|
||||
if (host.tunnelConnections) {
|
||||
try {
|
||||
// tunnelConnections is already parsed as an array from the backend
|
||||
const tunnelConnections = Array.isArray(host.tunnelConnections)
|
||||
? host.tunnelConnections
|
||||
: JSON.parse(host.tunnelConnections);
|
||||
if (Array.isArray(tunnelConnections)) {
|
||||
totalTunnelsCount += tunnelConnections.length;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
setTotalTunnels(totalTunnelsCount);
|
||||
@@ -190,13 +180,11 @@ export function Dashboard({
|
||||
const credentials = await getCredentials();
|
||||
setTotalCredentials(credentials.length);
|
||||
|
||||
// Fetch recent activity (35 items)
|
||||
setRecentActivityLoading(true);
|
||||
const activity = await getRecentActivity(35);
|
||||
setRecentActivity(activity);
|
||||
setRecentActivityLoading(false);
|
||||
|
||||
// Fetch server stats for first 5 servers
|
||||
setServerStatsLoading(true);
|
||||
const serversWithStats = await Promise.all(
|
||||
hosts.slice(0, 5).map(async (host: { id: number; name: string }) => {
|
||||
@@ -229,12 +217,10 @@ export function Dashboard({
|
||||
|
||||
fetchDashboardData();
|
||||
|
||||
// Refresh every 30 seconds
|
||||
const interval = setInterval(fetchDashboardData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [loggedIn]);
|
||||
|
||||
// Handler for resetting recent activity
|
||||
const handleResetActivity = async () => {
|
||||
try {
|
||||
await resetRecentActivity();
|
||||
@@ -244,9 +230,7 @@ export function Dashboard({
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for opening a recent activity item
|
||||
const handleActivityClick = (item: RecentActivityItem) => {
|
||||
// Find the host and open appropriate tab
|
||||
getSSHHosts().then((hosts) => {
|
||||
const host = hosts.find((h: { id: number }) => h.id === item.hostId);
|
||||
if (!host) return;
|
||||
@@ -267,7 +251,6 @@ export function Dashboard({
|
||||
});
|
||||
};
|
||||
|
||||
// Quick Actions handlers
|
||||
const handleAddHost = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
|
||||
@@ -226,9 +226,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const currentLoadingPathRef = useRef<string>("");
|
||||
const keepaliveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const activityLoggedRef = useRef(false);
|
||||
const activityLoggingRef = useRef(false); // Prevent concurrent logging calls
|
||||
const activityLoggingRef = useRef(false);
|
||||
|
||||
// Centralized activity logging to prevent duplicates
|
||||
const logFileManagerActivity = useCallback(async () => {
|
||||
if (
|
||||
!currentHost?.id ||
|
||||
@@ -238,7 +237,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set flags IMMEDIATELY to prevent race conditions
|
||||
activityLoggingRef.current = true;
|
||||
activityLoggedRef.current = true;
|
||||
|
||||
@@ -246,10 +244,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const hostName =
|
||||
currentHost.name || `${currentHost.username}@${currentHost.ip}`;
|
||||
await logActivity("file_manager", currentHost.id, hostName);
|
||||
// Don't reset activityLoggedRef on success - we want to prevent future calls
|
||||
} catch (err) {
|
||||
console.warn("Failed to log file manager activity:", err);
|
||||
// Reset on error so it can be retried
|
||||
activityLoggedRef.current = false;
|
||||
} finally {
|
||||
activityLoggingRef.current = false;
|
||||
@@ -350,8 +346,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
clearSelection();
|
||||
initialLoadDoneRef.current = true;
|
||||
|
||||
// Log activity for recent connections (after successful directory load)
|
||||
// Only log if TOTP was not required (if TOTP is required, we'll log after verification)
|
||||
if (!result?.requires_totp) {
|
||||
logFileManagerActivity();
|
||||
}
|
||||
@@ -1306,7 +1300,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
initialLoadDoneRef.current = true;
|
||||
toast.success(t("fileManager.connectedSuccessfully"));
|
||||
|
||||
// Log activity for recent connections (after successful directory load)
|
||||
logFileManagerActivity();
|
||||
} catch (dirError: unknown) {
|
||||
console.error("Failed to load initial directory:", dirError);
|
||||
|
||||
@@ -34,21 +34,16 @@ export function HostManager({
|
||||
const ignoreNextHostConfigChangeRef = useRef<boolean>(false);
|
||||
const lastProcessedHostIdRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Update editing host when hostConfig prop changes (from sidebar edit button)
|
||||
useEffect(() => {
|
||||
// Skip if we should ignore this change
|
||||
if (ignoreNextHostConfigChangeRef.current) {
|
||||
ignoreNextHostConfigChangeRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process if this is an external edit request (from sidebar)
|
||||
if (hostConfig && initialTab === "add_host") {
|
||||
const currentHostId = hostConfig.id;
|
||||
|
||||
// Open editor if it's a different host OR same host but user is on viewer/credentials tabs
|
||||
if (currentHostId !== lastProcessedHostIdRef.current) {
|
||||
// Different host - always open
|
||||
setEditingHost(hostConfig);
|
||||
setActiveTab("add_host");
|
||||
lastProcessedHostIdRef.current = currentHostId;
|
||||
@@ -57,11 +52,9 @@ export function HostManager({
|
||||
activeTab === "credentials" ||
|
||||
activeTab === "add_credential"
|
||||
) {
|
||||
// Same host but user manually navigated away - reopen
|
||||
setEditingHost(hostConfig);
|
||||
setActiveTab("add_host");
|
||||
}
|
||||
// If same host and already on add_host tab, do nothing (don't block tab changes)
|
||||
}
|
||||
}, [hostConfig, initialTab]);
|
||||
|
||||
@@ -72,11 +65,9 @@ export function HostManager({
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
// Ignore the next hostConfig change (which will come from ssh-hosts:changed event)
|
||||
ignoreNextHostConfigChangeRef.current = true;
|
||||
setEditingHost(null);
|
||||
setActiveTab("host_viewer");
|
||||
// Clear after a delay so the same host can be edited again
|
||||
setTimeout(() => {
|
||||
lastProcessedHostIdRef.current = undefined;
|
||||
}, 500);
|
||||
|
||||
@@ -129,7 +129,6 @@ export function HostManagerEditor({
|
||||
);
|
||||
const isSubmittingRef = useRef(false);
|
||||
|
||||
// Monitoring interval states
|
||||
const [statusIntervalUnit, setStatusIntervalUnit] = useState<
|
||||
"seconds" | "minutes"
|
||||
>("seconds");
|
||||
@@ -168,9 +167,7 @@ export function HostManagerEditor({
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
setSshConfigurations(uniqueConfigurations);
|
||||
} catch {
|
||||
// Failed to load hosts data
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
@@ -199,9 +196,7 @@ export function HostManagerEditor({
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
setSshConfigurations(uniqueConfigurations);
|
||||
} catch {
|
||||
// Failed to reload hosts after credential change
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
window.addEventListener("credentials:changed", handleCredentialChange);
|
||||
@@ -319,7 +314,6 @@ export function HostManagerEditor({
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.authType === "none") {
|
||||
// No credentials required for "none" auth type - will use keyboard-interactive
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,7 +438,6 @@ export function HostManagerEditor({
|
||||
: "none";
|
||||
setAuthTab(defaultAuthType);
|
||||
|
||||
// Parse statsConfig from JSON string if needed
|
||||
let parsedStatsConfig = DEFAULT_STATS_CONFIG;
|
||||
try {
|
||||
if (cleanedHost.statsConfig) {
|
||||
@@ -457,7 +450,6 @@ export function HostManagerEditor({
|
||||
console.error("Failed to parse statsConfig:", error);
|
||||
}
|
||||
|
||||
// Merge with defaults to ensure all new fields are present
|
||||
parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig };
|
||||
|
||||
const formData = {
|
||||
@@ -552,7 +544,6 @@ export function HostManagerEditor({
|
||||
data.name = `${data.username}@${data.ip}`;
|
||||
}
|
||||
|
||||
// Validate monitoring intervals
|
||||
if (data.statsConfig) {
|
||||
const statusInterval = data.statsConfig.statusCheckInterval || 30;
|
||||
const metricsInterval = data.statsConfig.metricsInterval || 30;
|
||||
@@ -663,7 +654,6 @@ export function HostManagerEditor({
|
||||
|
||||
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
||||
|
||||
// Refresh backend polling to pick up new/updated host configuration
|
||||
const { refreshServerPolling } = await import("@/ui/main-axios.ts");
|
||||
refreshServerPolling();
|
||||
} catch {
|
||||
@@ -1391,7 +1381,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Font Family */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.fontFamily"
|
||||
@@ -1425,7 +1414,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Font Size */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.fontSize"
|
||||
@@ -1450,7 +1438,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Letter Spacing */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.letterSpacing"
|
||||
@@ -1477,7 +1464,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Line Height */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.lineHeight"
|
||||
@@ -1502,7 +1488,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Cursor Style */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.cursorStyle"
|
||||
@@ -1533,7 +1518,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Cursor Blink */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.cursorBlink"
|
||||
@@ -1557,11 +1541,9 @@ export function HostManagerEditor({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Behavior Settings */}
|
||||
<AccordionItem value="behavior">
|
||||
<AccordionTrigger>Behavior</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 pt-4">
|
||||
{/* Scrollback Buffer */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.scrollback"
|
||||
@@ -1588,7 +1570,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Bell Style */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.bellStyle"
|
||||
@@ -1623,7 +1604,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Right Click Selects Word */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.rightClickSelectsWord"
|
||||
@@ -1645,7 +1625,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Fast Scroll Modifier */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.fastScrollModifier"
|
||||
@@ -1674,7 +1653,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Fast Scroll Sensitivity */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.fastScrollSensitivity"
|
||||
@@ -1701,7 +1679,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Minimum Contrast Ratio */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.minimumContrastRatio"
|
||||
@@ -1731,11 +1708,9 @@ export function HostManagerEditor({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<AccordionItem value="advanced">
|
||||
<AccordionTrigger>Advanced</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 pt-4">
|
||||
{/* Agent Forwarding */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.agentForwarding"
|
||||
@@ -1758,7 +1733,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Backspace Mode */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.backspaceMode"
|
||||
@@ -1790,7 +1764,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Startup Snippet */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.startupSnippetId"
|
||||
@@ -1862,7 +1835,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Auto MOSH */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="terminalConfig.autoMosh"
|
||||
@@ -1884,7 +1856,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* MOSH Command */}
|
||||
{form.watch("terminalConfig.autoMosh") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -1906,7 +1877,6 @@ export function HostManagerEditor({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Environment Variables
|
||||
@@ -2395,9 +2365,7 @@ export function HostManagerEditor({
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="statistics" className="space-y-6">
|
||||
{/* Monitoring Configuration Section */}
|
||||
<div className="space-y-4">
|
||||
{/* Status Check Monitoring */}
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -2463,7 +2431,6 @@ export function HostManagerEditor({
|
||||
value: "seconds" | "minutes",
|
||||
) => {
|
||||
setStatusIntervalUnit(value);
|
||||
// Convert current value to new unit
|
||||
const currentSeconds = field.value || 30;
|
||||
if (value === "minutes") {
|
||||
const minutes = Math.round(
|
||||
@@ -2496,7 +2463,6 @@ export function HostManagerEditor({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metrics Monitoring */}
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -2560,7 +2526,6 @@ export function HostManagerEditor({
|
||||
value: "seconds" | "minutes",
|
||||
) => {
|
||||
setMetricsIntervalUnit(value);
|
||||
// Convert current value to new unit
|
||||
const currentSeconds = field.value || 30;
|
||||
if (value === "minutes") {
|
||||
const minutes = Math.round(
|
||||
@@ -2594,7 +2559,6 @@ export function HostManagerEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Only show widget selection if metrics monitoring is enabled */}
|
||||
{form.watch("statsConfig.metricsEnabled") && (
|
||||
<>
|
||||
<FormField
|
||||
|
||||
@@ -126,7 +126,6 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
await fetchHosts();
|
||||
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
||||
|
||||
// Refresh backend polling to remove deleted host
|
||||
const { refreshServerPolling } = await import("@/ui/main-axios.ts");
|
||||
refreshServerPolling();
|
||||
} catch {
|
||||
@@ -392,7 +391,6 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to parse stats config and format monitoring status
|
||||
const getMonitoringStatus = (host: SSHHost) => {
|
||||
try {
|
||||
const statsConfig = host.statsConfig
|
||||
|
||||
@@ -80,7 +80,6 @@ export function Server({
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||
const [showStatsUI, setShowStatsUI] = React.useState(true);
|
||||
|
||||
// Parse stats config for monitoring settings
|
||||
const statsConfig = React.useMemo((): StatsConfig => {
|
||||
if (!currentHostConfig?.statsConfig) {
|
||||
return DEFAULT_STATS_CONFIG;
|
||||
@@ -181,7 +180,6 @@ export function Server({
|
||||
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
|
||||
}, [hostConfig?.id]);
|
||||
|
||||
// Separate effect for status monitoring
|
||||
React.useEffect(() => {
|
||||
if (!statusCheckEnabled || !currentHostConfig?.id || !isVisible) {
|
||||
setServerStatus("offline");
|
||||
@@ -207,7 +205,6 @@ export function Server({
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("offline");
|
||||
} else if (err?.response?.status === 404) {
|
||||
// Status not available - monitoring disabled
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -217,7 +214,7 @@ export function Server({
|
||||
};
|
||||
|
||||
fetchStatus();
|
||||
intervalId = window.setInterval(fetchStatus, 10000); // Poll backend every 10 seconds
|
||||
intervalId = window.setInterval(fetchStatus, 10000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -225,7 +222,6 @@ export function Server({
|
||||
};
|
||||
}, [currentHostConfig?.id, isVisible, statusCheckEnabled]);
|
||||
|
||||
// Separate effect for metrics monitoring
|
||||
React.useEffect(() => {
|
||||
if (!metricsEnabled || !currentHostConfig?.id || !isVisible) {
|
||||
setShowStatsUI(false);
|
||||
@@ -244,7 +240,6 @@ export function Server({
|
||||
setMetrics(data);
|
||||
setMetricsHistory((prev) => {
|
||||
const newHistory = [...prev, data];
|
||||
// Keep last 20 data points for chart
|
||||
return newHistory.slice(-20);
|
||||
});
|
||||
setShowStatsUI(true);
|
||||
@@ -256,7 +251,6 @@ export function Server({
|
||||
response?: { status?: number; data?: { error?: string } };
|
||||
};
|
||||
if (err?.response?.status === 404) {
|
||||
// Metrics not available - monitoring disabled
|
||||
setMetrics(null);
|
||||
setShowStatsUI(false);
|
||||
} else if (
|
||||
@@ -281,7 +275,7 @@ export function Server({
|
||||
};
|
||||
|
||||
fetchMetrics();
|
||||
intervalId = window.setInterval(fetchMetrics, 10000); // Poll backend every 10 seconds
|
||||
intervalId = window.setInterval(fetchMetrics, 10000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -22,7 +22,6 @@ interface CpuWidgetProps {
|
||||
export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = React.useMemo(() => {
|
||||
return metricsHistory.map((m, index) => ({
|
||||
index,
|
||||
|
||||
@@ -15,7 +15,6 @@ interface DiskWidgetProps {
|
||||
export function DiskWidget({ metrics }: DiskWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Prepare radial chart data
|
||||
const radialData = React.useMemo(() => {
|
||||
const percent = metrics?.disk?.percent || 0;
|
||||
return [
|
||||
|
||||
@@ -22,7 +22,6 @@ interface MemoryWidgetProps {
|
||||
export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = React.useMemo(() => {
|
||||
return metricsHistory.map((m, index) => ({
|
||||
index,
|
||||
|
||||
@@ -73,7 +73,6 @@ export function SnippetsSidebar({
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getSnippets();
|
||||
// Defensive: ensure data is an array
|
||||
setSnippets(Array.isArray(data) ? data : []);
|
||||
} catch {
|
||||
toast.error(t("snippets.failedToFetch"));
|
||||
@@ -118,7 +117,6 @@ export function SnippetsSidebar({
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate required fields
|
||||
const errors = {
|
||||
name: !formData.name.trim(),
|
||||
content: !formData.content.trim(),
|
||||
@@ -159,7 +157,6 @@ export function SnippetsSidebar({
|
||||
|
||||
const handleExecute = (snippet: Snippet) => {
|
||||
if (selectedTabIds.length > 0) {
|
||||
// Execute on selected terminals
|
||||
selectedTabIds.forEach((tabId) => {
|
||||
const tab = tabs.find((t: TabData) => t.id === tabId);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
@@ -173,7 +170,6 @@ export function SnippetsSidebar({
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Execute on current terminal (legacy behavior)
|
||||
onExecute(snippet.content);
|
||||
toast.success(t("snippets.executeSuccess", { name: snippet.name }));
|
||||
}
|
||||
@@ -190,7 +186,6 @@ export function SnippetsSidebar({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay and Sidebar */}
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] flex justify-end pointer-events-auto isolate"
|
||||
style={{
|
||||
@@ -207,7 +202,6 @@ export function SnippetsSidebar({
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-border">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{t("snippets.title")}
|
||||
@@ -223,10 +217,8 @@ export function SnippetsSidebar({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Terminal Selection */}
|
||||
{terminalTabs.length > 0 && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -386,7 +378,6 @@ export function SnippetsSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create/Edit Dialog - centered modal */}
|
||||
{showDialog && (
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center z-[9999999] bg-black/50 backdrop-blur-sm"
|
||||
|
||||
@@ -122,14 +122,13 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const isConnectingRef = useRef(false);
|
||||
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const activityLoggedRef = useRef(false);
|
||||
const activityLoggingRef = useRef(false); // Prevent concurrent logging calls
|
||||
const activityLoggingRef = useRef(false);
|
||||
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const DEBOUNCE_MS = 140;
|
||||
|
||||
// Centralized activity logging to prevent duplicates
|
||||
const logTerminalActivity = async () => {
|
||||
if (
|
||||
!hostConfig.id ||
|
||||
@@ -139,7 +138,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Set flags IMMEDIATELY to prevent race conditions
|
||||
activityLoggingRef.current = true;
|
||||
activityLoggedRef.current = true;
|
||||
|
||||
@@ -147,10 +145,8 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const hostName =
|
||||
hostConfig.name || `${hostConfig.username}@${hostConfig.ip}`;
|
||||
await logActivity("terminal", hostConfig.id, hostName);
|
||||
// Don't reset activityLoggedRef on success - we want to prevent future calls
|
||||
} catch (err) {
|
||||
console.warn("Failed to log terminal activity:", err);
|
||||
// Reset on error so it can be retried
|
||||
activityLoggedRef.current = false;
|
||||
} finally {
|
||||
activityLoggingRef.current = false;
|
||||
@@ -193,9 +189,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
terminal as { refresh?: (start: number, end: number) => void }
|
||||
).refresh(0, terminal.rows - 1);
|
||||
}
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function performFit() {
|
||||
@@ -250,7 +244,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
keyPassword?: string;
|
||||
}) {
|
||||
if (webSocketRef.current && terminal) {
|
||||
// Send reconnect message with credentials
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "reconnect_with_credentials",
|
||||
@@ -335,9 +328,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
@@ -587,18 +578,14 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
reconnectAttempts.current = 0;
|
||||
isReconnectingRef.current = false;
|
||||
|
||||
// Log activity for recent connections
|
||||
logTerminalActivity();
|
||||
|
||||
// Execute post-connection actions
|
||||
setTimeout(async () => {
|
||||
// Merge default config with host-specific config
|
||||
const terminalConfig = {
|
||||
...DEFAULT_TERMINAL_CONFIG,
|
||||
...hostConfig.terminalConfig,
|
||||
};
|
||||
|
||||
// Set environment variables
|
||||
if (
|
||||
terminalConfig.environmentVariables &&
|
||||
terminalConfig.environmentVariables.length > 0
|
||||
@@ -616,7 +603,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
}
|
||||
|
||||
// Execute startup snippet
|
||||
if (terminalConfig.startupSnippetId) {
|
||||
try {
|
||||
const snippets = await getSnippets();
|
||||
@@ -638,7 +624,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
}
|
||||
|
||||
// Execute MOSH command
|
||||
if (terminalConfig.autoMosh && ws.readyState === 1) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -675,8 +660,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
} else if (msg.type === "keyboard_interactive_available") {
|
||||
// Keyboard-interactive auth is available (e.g., Warpgate OIDC)
|
||||
// Show terminal immediately so user can see auth prompts
|
||||
setKeyboardInteractiveDetected(true);
|
||||
setIsConnecting(false);
|
||||
if (connectionTimeoutRef.current) {
|
||||
@@ -684,8 +667,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
} else if (msg.type === "auth_method_not_available") {
|
||||
// Server doesn't support keyboard-interactive for "none" auth
|
||||
// Show SSHAuthDialog for manual credential entry
|
||||
setAuthDialogReason("no_keyboard");
|
||||
setShowAuthDialog(true);
|
||||
setIsConnecting(false);
|
||||
@@ -751,9 +732,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Clipboard API not available, fallback to textarea method
|
||||
}
|
||||
} catch {}
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
@@ -773,26 +752,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
return await navigator.clipboard.readText();
|
||||
}
|
||||
} catch {
|
||||
// Clipboard read not available or not permitted
|
||||
}
|
||||
} catch {}
|
||||
return "";
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal || !xtermRef.current) return;
|
||||
|
||||
// Merge default config with host-specific config
|
||||
const config = {
|
||||
...DEFAULT_TERMINAL_CONFIG,
|
||||
...hostConfig.terminalConfig,
|
||||
};
|
||||
|
||||
// Get theme colors
|
||||
const themeColors =
|
||||
TERMINAL_THEMES[config.theme]?.colors || TERMINAL_THEMES.termix.colors;
|
||||
|
||||
// Get font family with fallback
|
||||
const fontConfig = TERMINAL_FONTS.find(
|
||||
(f) => f.value === config.fontFamily,
|
||||
);
|
||||
@@ -875,9 +849,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const pasteText = await readTextFromClipboard();
|
||||
if (pasteText) terminal.paste(pasteText);
|
||||
}
|
||||
} catch {
|
||||
// Ignore clipboard operation errors
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
element?.addEventListener("contextmenu", handleContextMenu);
|
||||
|
||||
@@ -886,7 +858,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
|
||||
navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
|
||||
|
||||
// Handle backspace mode (Control-H)
|
||||
if (
|
||||
config.backspaceMode === "control-h" &&
|
||||
e.key === "Backspace" &&
|
||||
@@ -943,7 +914,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
resizeTimeout.current = setTimeout(() => {
|
||||
if (!isVisibleRef.current || !isReady) return;
|
||||
performFit();
|
||||
}, 50); // Reduced from 150ms to 50ms for snappier response
|
||||
}, 50);
|
||||
});
|
||||
|
||||
resizeObserver.observe(xtermRef.current);
|
||||
@@ -1022,31 +993,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || !isReady || !fitAddonRef.current || !terminal) {
|
||||
// Reset fitted state when becoming invisible
|
||||
if (!isVisible && isFitted) {
|
||||
setIsFitted(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// When becoming visible, we need to:
|
||||
// 1. Mark as not fitted
|
||||
// 2. Clear any rendering artifacts
|
||||
// 3. Fit to the container size
|
||||
// 4. Mark as fitted (happens in performFit)
|
||||
setIsFitted(false);
|
||||
|
||||
// Use double requestAnimationFrame to ensure container has laid out
|
||||
let rafId1: number;
|
||||
let rafId2: number;
|
||||
|
||||
rafId1 = requestAnimationFrame(() => {
|
||||
rafId2 = requestAnimationFrame(() => {
|
||||
// Force a hard refresh to clear any artifacts
|
||||
hardRefresh();
|
||||
// Fit the terminal to the new size
|
||||
performFit();
|
||||
// Focus will happen after isFitted becomes true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1056,7 +1017,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
};
|
||||
}, [isVisible, isReady, splitScreen, terminal]);
|
||||
|
||||
// Focus the terminal after it's been fitted and is visible
|
||||
useEffect(() => {
|
||||
if (
|
||||
isFitted &&
|
||||
@@ -1066,7 +1026,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
terminal &&
|
||||
!splitScreen
|
||||
) {
|
||||
// Use requestAnimationFrame to ensure the terminal is actually visible in the DOM
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
terminal.focus();
|
||||
});
|
||||
@@ -1131,7 +1090,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = `
|
||||
/* Import popular terminal fonts from Google Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||
|
||||
@@ -192,7 +192,6 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||
|
||||
await fetchTunnelStatuses();
|
||||
} catch {
|
||||
// Ignore tunnel action errors
|
||||
} finally {
|
||||
setTunnelActions((prev) => ({ ...prev, [tunnelName]: false }));
|
||||
}
|
||||
|
||||
@@ -58,16 +58,12 @@ export function Auth({
|
||||
}: AuthProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Detect if we're running in Electron's WebView/iframe
|
||||
const isInElectronWebView = () => {
|
||||
try {
|
||||
// Check if we're in an iframe AND the parent is Electron
|
||||
if (window.self !== window.top) {
|
||||
// We're in an iframe, likely Electron's ElectronLoginForm
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin iframe, can't access parent
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
@@ -108,7 +104,6 @@ export function Auth({
|
||||
}, [loggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip when in Electron WebView iframe
|
||||
if (isInElectronWebView()) {
|
||||
return;
|
||||
}
|
||||
@@ -119,7 +114,6 @@ export function Auth({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip when in Electron WebView iframe
|
||||
if (isInElectronWebView()) {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +130,6 @@ export function Auth({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip when in Electron WebView iframe
|
||||
if (isInElectronWebView()) {
|
||||
return;
|
||||
}
|
||||
@@ -159,8 +152,6 @@ export function Auth({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip database health check when in Electron WebView iframe
|
||||
// The parent Electron window will handle authentication
|
||||
if (isInElectronWebView()) {
|
||||
setDbHealthChecking(false);
|
||||
setDbConnectionFailed(false);
|
||||
@@ -615,7 +606,6 @@ export function Auth({
|
||||
);
|
||||
}
|
||||
|
||||
// Show ElectronLoginForm when Electron has a configured server and user is not logged in
|
||||
if (isElectron() && currentServerUrl && !loggedIn && !authLoading) {
|
||||
return (
|
||||
<div
|
||||
@@ -797,7 +787,6 @@ export function Auth({
|
||||
{!loggedIn && !authLoading && !totpRequired && (
|
||||
<>
|
||||
{(() => {
|
||||
// Check if any authentication method is available
|
||||
const hasLogin = passwordLoginAllowed && !firstUser;
|
||||
const hasSignup =
|
||||
(passwordLoginAllowed || firstUser) && registrationAllowed;
|
||||
|
||||
@@ -25,9 +25,7 @@ export function ElectronLoginForm({
|
||||
const [currentUrl, setCurrentUrl] = useState(serverUrl);
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for messages from iframe
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
// Only accept messages from our configured server
|
||||
try {
|
||||
const serverOrigin = new URL(serverUrl).origin;
|
||||
if (event.origin !== serverOrigin) {
|
||||
@@ -43,25 +41,17 @@ export function ElectronLoginForm({
|
||||
!hasAuthenticatedRef.current &&
|
||||
!isAuthenticating
|
||||
) {
|
||||
console.log(
|
||||
"[ElectronLoginForm] Received auth success from iframe",
|
||||
);
|
||||
hasAuthenticatedRef.current = true;
|
||||
setIsAuthenticating(true);
|
||||
|
||||
try {
|
||||
// Save JWT to localStorage (Electron mode)
|
||||
localStorage.setItem("jwt", data.token);
|
||||
|
||||
// Verify it was saved
|
||||
const savedToken = localStorage.getItem("jwt");
|
||||
if (!savedToken) {
|
||||
throw new Error("Failed to save JWT to localStorage");
|
||||
}
|
||||
|
||||
console.log("[ElectronLoginForm] JWT saved successfully");
|
||||
|
||||
// Small delay to ensure everything is saved
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
onAuthSuccess();
|
||||
@@ -86,37 +76,29 @@ export function ElectronLoginForm({
|
||||
}, [serverUrl, isAuthenticating, onAuthSuccess, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// Inject script into iframe when it loads
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
|
||||
const handleLoad = () => {
|
||||
setLoading(false);
|
||||
|
||||
// Update current URL when iframe loads
|
||||
try {
|
||||
if (iframe.contentWindow) {
|
||||
setCurrentUrl(iframe.contentWindow.location.href);
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin, can't access - use serverUrl
|
||||
setCurrentUrl(serverUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
// Inject JavaScript to detect JWT
|
||||
const injectedScript = `
|
||||
(function() {
|
||||
console.log('[Electron WebView] Script injected');
|
||||
|
||||
let hasNotified = false;
|
||||
|
||||
function postJWTToParent(token, source) {
|
||||
if (hasNotified) return;
|
||||
hasNotified = true;
|
||||
|
||||
console.log('[Electron WebView] Posting JWT to parent, source:', source);
|
||||
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: 'AUTH_SUCCESS',
|
||||
@@ -163,7 +145,6 @@ export function ElectronLoginForm({
|
||||
return false;
|
||||
}
|
||||
|
||||
// Intercept localStorage.setItem
|
||||
const originalSetItem = localStorage.setItem;
|
||||
localStorage.setItem = function(key, value) {
|
||||
originalSetItem.apply(this, arguments);
|
||||
@@ -172,7 +153,6 @@ export function ElectronLoginForm({
|
||||
}
|
||||
};
|
||||
|
||||
// Intercept sessionStorage.setItem
|
||||
const originalSessionSetItem = sessionStorage.setItem;
|
||||
sessionStorage.setItem = function(key, value) {
|
||||
originalSessionSetItem.apply(this, arguments);
|
||||
@@ -181,7 +161,6 @@ export function ElectronLoginForm({
|
||||
}
|
||||
};
|
||||
|
||||
// Poll for JWT
|
||||
const intervalId = setInterval(() => {
|
||||
if (hasNotified) {
|
||||
clearInterval(intervalId);
|
||||
@@ -192,17 +171,14 @@ export function ElectronLoginForm({
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Stop after 5 minutes
|
||||
setTimeout(() => {
|
||||
clearInterval(intervalId);
|
||||
}, 300000);
|
||||
|
||||
// Initial check
|
||||
checkAuth();
|
||||
})();
|
||||
`;
|
||||
|
||||
// Try to inject the script
|
||||
try {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
@@ -210,11 +186,9 @@ export function ElectronLoginForm({
|
||||
"*",
|
||||
);
|
||||
|
||||
// Also try direct execution if same origin
|
||||
iframe.contentWindow.eval(injectedScript);
|
||||
}
|
||||
} catch (err) {
|
||||
// Cross-origin restrictions - this is expected for external servers
|
||||
console.warn(
|
||||
"[ElectronLoginForm] Cannot inject script due to cross-origin restrictions",
|
||||
);
|
||||
@@ -250,12 +224,10 @@ export function ElectronLoginForm({
|
||||
onChangeServer();
|
||||
};
|
||||
|
||||
// Format URL for display (remove protocol)
|
||||
const displayUrl = currentUrl.replace(/^https?:\/\//, "");
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 w-screen h-screen bg-dark-bg flex flex-col">
|
||||
{/* Navigation Bar */}
|
||||
<div className="flex items-center justify-between p-4 bg-dark-bg border-b border-dark-border">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
|
||||
@@ -37,9 +37,7 @@ export function ElectronServerConfig({
|
||||
if (config?.serverUrl) {
|
||||
setServerUrl(config.serverUrl);
|
||||
}
|
||||
} catch {
|
||||
// Ignore config loading errors
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
@@ -54,7 +52,6 @@ export function ElectronServerConfig({
|
||||
try {
|
||||
let normalizedUrl = serverUrl.trim();
|
||||
|
||||
// Ensure URL has http:// or https://
|
||||
if (
|
||||
!normalizedUrl.startsWith("http://") &&
|
||||
!normalizedUrl.startsWith("https://")
|
||||
|
||||
@@ -60,9 +60,7 @@ function AppContent() {
|
||||
localStorage.setItem("topNavbarOpen", JSON.stringify(isTopbarOpen));
|
||||
}, [isTopbarOpen]);
|
||||
|
||||
const handleSelectView = () => {
|
||||
// View switching is now handled by tabs context
|
||||
};
|
||||
const handleSelectView = () => {};
|
||||
|
||||
const handleAuthSuccess = (authData: {
|
||||
isAdmin: boolean;
|
||||
|
||||
@@ -133,8 +133,6 @@ export function AppView({
|
||||
prev.splitScreenTabsStr !== allSplitScreenTab.join(",");
|
||||
const tabIdsChanged = prev.terminalTabIds !== currentTabIds;
|
||||
|
||||
// Only trigger hideThenFit if tabs were added/removed (not just reordered)
|
||||
// or if current tab or split screen changed
|
||||
const isJustReorder =
|
||||
!lengthChanged && tabIdsChanged && !currentTabChanged && !splitChanged;
|
||||
|
||||
@@ -145,7 +143,6 @@ export function AppView({
|
||||
hideThenFit();
|
||||
}
|
||||
|
||||
// Update the ref for next comparison
|
||||
prevStateRef.current = {
|
||||
terminalTabsLength: terminalTabs.length,
|
||||
currentTab,
|
||||
@@ -186,10 +183,8 @@ export function AppView({
|
||||
|
||||
const HEADER_H = 28;
|
||||
|
||||
// Create a stable map of terminal IDs to preserve component identity
|
||||
const terminalIdMapRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// Track all terminal IDs that have ever existed
|
||||
useEffect(() => {
|
||||
terminalTabs.forEach((t) => terminalIdMapRef.current.add(t.id));
|
||||
}, [terminalTabs]);
|
||||
@@ -240,8 +235,6 @@ export function AppView({
|
||||
});
|
||||
}
|
||||
|
||||
// Render in a STABLE order by ID to prevent React from unmounting
|
||||
// Sort by ID instead of array position
|
||||
const sortedTerminalTabs = [...terminalTabs].sort((a, b) => a.id - b.id);
|
||||
|
||||
return (
|
||||
@@ -628,7 +621,6 @@ export function AppView({
|
||||
const isTerminal = currentTabData?.type === "terminal";
|
||||
const isSplitScreen = allSplitScreenTab.length > 0;
|
||||
|
||||
// Get terminal background color for the current tab
|
||||
const terminalConfig = {
|
||||
...DEFAULT_TERMINAL_CONFIG,
|
||||
...(currentTabData?.hostConfig as any)?.terminalConfig,
|
||||
@@ -642,7 +634,6 @@ export function AppView({
|
||||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||||
const bottomMarginPx = 8;
|
||||
|
||||
// Determine background color based on current tab type
|
||||
let containerBackground = "var(--color-dark-bg)";
|
||||
if (isFileManager && !isSplitScreen) {
|
||||
containerBackground = "var(--color-dark-bg-darkest)";
|
||||
|
||||
@@ -33,12 +33,10 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
// Update host when prop changes
|
||||
useEffect(() => {
|
||||
setHost(initialHost);
|
||||
}, [initialHost]);
|
||||
|
||||
// Listen for host changes to immediately update config
|
||||
useEffect(() => {
|
||||
const handleHostsChanged = async () => {
|
||||
const { getSSHHosts } = await import("@/ui/main-axios.ts");
|
||||
@@ -54,7 +52,6 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
|
||||
}, [host.id]);
|
||||
|
||||
// Parse stats config for monitoring settings
|
||||
const statsConfig = useMemo(() => {
|
||||
try {
|
||||
return host.statsConfig
|
||||
@@ -68,7 +65,6 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
|
||||
|
||||
useEffect(() => {
|
||||
// Don't poll if status monitoring is disabled
|
||||
if (!shouldShowStatus) {
|
||||
setServerStatus("offline");
|
||||
return;
|
||||
@@ -90,7 +86,6 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("degraded");
|
||||
} else if (err?.response?.status === 404) {
|
||||
// Status not available - monitoring disabled
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -100,7 +95,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
};
|
||||
|
||||
fetchStatus();
|
||||
const intervalId = window.setInterval(fetchStatus, 10000); // Poll backend every 10 seconds
|
||||
const intervalId = window.setInterval(fetchStatus, 10000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -47,7 +47,6 @@ export function Tab({
|
||||
}: TabProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Firefox-style tab classes using cn utility
|
||||
const tabBaseClasses = cn(
|
||||
"relative flex items-center gap-1.5 px-3 w-full min-w-0",
|
||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||
@@ -65,7 +64,6 @@ export function Tab({
|
||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||
);
|
||||
|
||||
// Helper function to split title into base and suffix
|
||||
const splitTitle = (fullTitle: string): { base: string; suffix: string } => {
|
||||
const match = fullTitle.match(/^(.*?)(\s*\(\d+\))$/);
|
||||
if (match) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
@@ -97,24 +96,19 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
}
|
||||
|
||||
const addTab = (tabData: Omit<Tab, "id">): number => {
|
||||
// Check if an ssh_manager tab already exists
|
||||
if (tabData.type === "ssh_manager") {
|
||||
const existingTab = tabs.find((t) => t.type === "ssh_manager");
|
||||
if (existingTab) {
|
||||
// Update the existing tab with new data
|
||||
// Create a new object reference to force React to detect the change
|
||||
setTabs((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === existingTab.id
|
||||
? {
|
||||
...t,
|
||||
// Keep the original title (Host Manager)
|
||||
title: existingTab.title,
|
||||
hostConfig: tabData.hostConfig
|
||||
? { ...tabData.hostConfig }
|
||||
: undefined,
|
||||
initialTab: tabData.initialTab,
|
||||
// Add a timestamp to force re-render
|
||||
_updateTimestamp: Date.now(),
|
||||
}
|
||||
: t,
|
||||
@@ -222,7 +216,6 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) => {
|
||||
if (tab.hostConfig && tab.hostConfig.id === hostId) {
|
||||
// Don't update the title for ssh_manager tabs - they should stay as "Host Manager"
|
||||
if (tab.type === "ssh_manager") {
|
||||
return {
|
||||
...tab,
|
||||
@@ -230,7 +223,6 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
};
|
||||
}
|
||||
|
||||
// For other tabs (terminal, server, file_manager), update both config and title
|
||||
return {
|
||||
...tab,
|
||||
hostConfig: newHostConfig,
|
||||
|
||||
@@ -103,7 +103,7 @@ export function TopNavbar({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (justDroppedTabId !== null) {
|
||||
const timer = setTimeout(() => setJustDroppedTabId(null), 50); // Clear after a short delay
|
||||
const timer = setTimeout(() => setJustDroppedTabId(null), 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [justDroppedTabId]);
|
||||
@@ -138,7 +138,6 @@ export function TopNavbar({
|
||||
|
||||
const draggedIndex = dragState.draggedIndex;
|
||||
|
||||
// Build array of tab boundaries in ORIGINAL order
|
||||
const tabBoundaries: {
|
||||
index: number;
|
||||
start: number;
|
||||
@@ -158,25 +157,21 @@ export function TopNavbar({
|
||||
end: accumulatedX + tabWidth,
|
||||
mid: accumulatedX + tabWidth / 2,
|
||||
});
|
||||
accumulatedX += tabWidth + 4; // 4px gap
|
||||
accumulatedX += tabWidth + 4;
|
||||
});
|
||||
|
||||
if (tabBoundaries.length === 0) return null;
|
||||
|
||||
// Calculate the dragged tab's center in container coordinates
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const draggedTab = tabBoundaries[draggedIndex];
|
||||
// Convert absolute positions to container-relative coordinates
|
||||
const currentX = dragState.currentX - containerRect.left;
|
||||
const startX = dragState.startX - containerRect.left;
|
||||
const offset = currentX - startX;
|
||||
const draggedCenter = draggedTab.mid + offset;
|
||||
|
||||
// Determine target index based on where the dragged tab's center is
|
||||
let newTargetIndex = draggedIndex;
|
||||
|
||||
if (offset < 0) {
|
||||
// Moving left - find the leftmost tab whose midpoint we've passed
|
||||
for (let i = draggedIndex - 1; i >= 0; i--) {
|
||||
if (draggedCenter < tabBoundaries[i].mid) {
|
||||
newTargetIndex = i;
|
||||
@@ -185,7 +180,6 @@ export function TopNavbar({
|
||||
}
|
||||
}
|
||||
} else if (offset > 0) {
|
||||
// Moving right - find the rightmost tab whose midpoint we've passed
|
||||
for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) {
|
||||
if (draggedCenter > tabBoundaries[i].mid) {
|
||||
newTargetIndex = i;
|
||||
@@ -193,18 +187,14 @@ export function TopNavbar({
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Edge case: if dragged past the last tab, target should be at the very end
|
||||
const lastTabIndex = tabBoundaries.length - 1;
|
||||
if (lastTabIndex >= 0) {
|
||||
// Ensure there's at least one tab
|
||||
const lastTabEl = tabRefs.current.get(lastTabIndex);
|
||||
if (lastTabEl) {
|
||||
const lastTabRect = lastTabEl.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const lastTabEndInContainer = lastTabRect.right - containerRect.left;
|
||||
if (currentX > lastTabEndInContainer) {
|
||||
// When dragging past the last tab, insert at the very end
|
||||
// Use the last valid index (length - 1) not length itself
|
||||
newTargetIndex = lastTabIndex;
|
||||
}
|
||||
}
|
||||
@@ -217,13 +207,11 @@ export function TopNavbar({
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Firefox compatibility - track position via dragover
|
||||
if (dragState.draggedIndex === null) return;
|
||||
|
||||
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||
if (!containerRect) return;
|
||||
|
||||
// Update currentX if we have a valid clientX (Firefox may not provide it in onDrag)
|
||||
if (e.clientX !== 0) {
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
@@ -253,7 +241,6 @@ export function TopNavbar({
|
||||
if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) {
|
||||
prevTabsRef.current = tabs;
|
||||
|
||||
// Set animation flag and clear drag state synchronously
|
||||
flushSync(() => {
|
||||
setIsInDropAnimation(true);
|
||||
setDragState({
|
||||
@@ -356,14 +343,13 @@ export function TopNavbar({
|
||||
|
||||
const isDraggingThisTab = dragState.draggedIndex === index;
|
||||
const isTheDraggedTab = tab.id === dragState.draggedId;
|
||||
const isDroppedAndSnapping = tab.id === justDroppedTabId; // New condition
|
||||
const isDroppedAndSnapping = tab.id === justDroppedTabId;
|
||||
const dragOffset = isDraggingThisTab
|
||||
? dragState.currentX - dragState.startX
|
||||
: 0;
|
||||
|
||||
let transform = "";
|
||||
|
||||
// Skip all transforms if we just dropped to prevent glitches
|
||||
if (!isInDropAnimation) {
|
||||
if (isDraggingThisTab) {
|
||||
transform = `translateX(${dragOffset}px)`;
|
||||
@@ -374,13 +360,11 @@ export function TopNavbar({
|
||||
const draggedOriginalIndex = dragState.draggedIndex;
|
||||
const currentTargetIndex = dragState.targetIndex;
|
||||
|
||||
// Determine if this tab should shift left or right
|
||||
if (
|
||||
draggedOriginalIndex < currentTargetIndex && // Dragging rightwards
|
||||
index > draggedOriginalIndex && // This tab is to the right of the original position
|
||||
index <= currentTargetIndex // This tab is at or before the target position
|
||||
draggedOriginalIndex < currentTargetIndex &&
|
||||
index > draggedOriginalIndex &&
|
||||
index <= currentTargetIndex
|
||||
) {
|
||||
// Shift left to make space
|
||||
const draggedTabWidth =
|
||||
tabRefs.current
|
||||
.get(draggedOriginalIndex)
|
||||
@@ -388,11 +372,10 @@ export function TopNavbar({
|
||||
const gap = 4;
|
||||
transform = `translateX(-${draggedTabWidth + gap}px)`;
|
||||
} else if (
|
||||
draggedOriginalIndex > currentTargetIndex && // Dragging leftwards
|
||||
index >= currentTargetIndex && // This tab is at or after the target position
|
||||
index < draggedOriginalIndex // This tab is to the left of the original position
|
||||
draggedOriginalIndex > currentTargetIndex &&
|
||||
index >= currentTargetIndex &&
|
||||
index < draggedOriginalIndex
|
||||
) {
|
||||
// Shift right to make space
|
||||
const draggedTabWidth =
|
||||
tabRefs.current
|
||||
.get(draggedOriginalIndex)
|
||||
@@ -424,7 +407,6 @@ export function TopNavbar({
|
||||
onDragEnd={handleDragEnd}
|
||||
e
|
||||
onMouseDown={(e) => {
|
||||
// Middle mouse button (button === 1)
|
||||
if (e.button === 1 && !disableClose) {
|
||||
e.preventDefault();
|
||||
handleTabClose(tab.id);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
|
||||
@@ -101,9 +101,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
terminal as { refresh?: (start: number, end: number) => void }
|
||||
).refresh(0, terminal.rows - 1);
|
||||
}
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function performFit() {
|
||||
@@ -177,9 +175,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
@@ -229,9 +225,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore message parsing errors
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
|
||||
@@ -27,23 +27,16 @@ import {
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
|
||||
/**
|
||||
* Detect if we're running inside a React Native WebView
|
||||
*/
|
||||
function isReactNativeWebView(): boolean {
|
||||
return typeof window !== "undefined" && !!(window as any).ReactNativeWebView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post JWT token to React Native WebView for mobile app authentication
|
||||
*/
|
||||
function postJWTToWebView() {
|
||||
if (!isReactNativeWebView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get JWT from localStorage or cookies
|
||||
const jwt = getCookie("jwt") || localStorage.getItem("jwt");
|
||||
|
||||
if (!jwt) {
|
||||
@@ -51,7 +44,6 @@ function postJWTToWebView() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Post message to React Native
|
||||
(window as any).ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
type: "AUTH_SUCCESS",
|
||||
@@ -263,7 +255,6 @@ export function Auth({
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
|
||||
// Post JWT to React Native WebView if running in mobile app
|
||||
postJWTToWebView();
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -431,7 +422,6 @@ export function Auth({
|
||||
userId: res.userId || null,
|
||||
});
|
||||
|
||||
// Post JWT to React Native WebView if running in mobile app
|
||||
postJWTToWebView();
|
||||
}, 100);
|
||||
|
||||
@@ -521,7 +511,6 @@ export function Auth({
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
|
||||
// Post JWT to React Native WebView if running in mobile app
|
||||
postJWTToWebView();
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -670,7 +659,6 @@ export function Auth({
|
||||
{!internalLoggedIn && !authLoading && !totpRequired && (
|
||||
<>
|
||||
{(() => {
|
||||
// Check if any authentication method is available
|
||||
const hasLogin = passwordLoginAllowed && !firstUser;
|
||||
const hasSignup =
|
||||
(passwordLoginAllowed || firstUser) && registrationAllowed;
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
// Parse stats config for monitoring settings
|
||||
const statsConfig = useMemo(() => {
|
||||
try {
|
||||
return host.statsConfig
|
||||
@@ -34,7 +33,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
|
||||
|
||||
useEffect(() => {
|
||||
// Don't poll if status monitoring is disabled
|
||||
if (!shouldShowStatus) {
|
||||
setServerStatus("offline");
|
||||
return;
|
||||
@@ -56,7 +54,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("degraded");
|
||||
} else if (err?.response?.status === 404) {
|
||||
// Status not available - monitoring disabled
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -67,7 +64,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
const intervalId = window.setInterval(fetchStatus, 10000); // Poll backend every 10 seconds
|
||||
const intervalId = window.setInterval(fetchStatus, 10000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
|
||||
@@ -48,9 +48,7 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
||||
store.put({ handle: dirHandle }, "lastSaveDir");
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Failed to save directory handle
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const isFileSystemAPISupported = () => {
|
||||
|
||||
@@ -323,7 +323,6 @@ function createApiInstance(
|
||||
if (isSessionExpired && typeof window !== "undefined") {
|
||||
console.warn("Session expired - please log in again");
|
||||
|
||||
// Clear the JWT cookie to prevent reload loop
|
||||
document.cookie =
|
||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user