feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
This commit was merged in pull request #503.
This commit is contained in:
ZacharyZcR
2026-01-13 13:57:09 +08:00
committed by GitHub
parent 7caa32b364
commit aea87be4d3
2 changed files with 31 additions and 16 deletions

View File

@@ -211,7 +211,7 @@ class UserDataImport {
newHostData,
targetUserId,
options.userDataKey,
);
) as Record<string, unknown>;
}
delete processedHostData.id;
@@ -294,7 +294,7 @@ class UserDataImport {
newCredentialData,
targetUserId,
options.userDataKey,
);
) as Record<string, unknown>;
}
delete processedCredentialData.id;

View File

@@ -13,6 +13,7 @@ import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx";
import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx";
import { NetworkGraphView } from "@/ui/desktop/dashboard/network-graph";
import { Toaster } from "@/components/ui/sonner.tsx";
import { toast } from "sonner";
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts";
import { useTheme } from "@/components/theme-provider";
@@ -89,30 +90,44 @@ function AppContent() {
useEffect(() => {
const path = window.location.pathname;
const match = path.match(/^\/hosts\/([a-zA-Z0-9_-]+)\/terminal$/);
if (match) {
const hostId = match[1];
const openTerminalForHost = async () => {
// New format: /terminal/{hostNameOrId}
const terminalMatch = path.match(/^\/terminal\/([a-zA-Z0-9_-]+)$/);
// Legacy format: /hosts/{id}/terminal (backward compatible)
const legacyMatch = path.match(/^\/hosts\/([a-zA-Z0-9_-]+)\/terminal$/);
const hostIdentifier = terminalMatch?.[1] || legacyMatch?.[1];
if (hostIdentifier) {
const openTerminal = async () => {
try {
const { getSSHHostById } = await import("@/ui/main-axios.ts");
const host = await getSSHHostById(parseInt(hostId, 10));
const { getSSHHostById, getSSHHosts } = await import("@/ui/main-axios.ts");
let host = null;
// Pure numeric → lookup by ID
if (/^\d+$/.test(hostIdentifier)) {
host = await getSSHHostById(parseInt(hostIdentifier, 10));
} else {
// Non-numeric → lookup by name (first match)
const hosts = await getSSHHosts();
host = hosts.find((h: { name?: string }) => h.name === hostIdentifier) || null;
}
if (host) {
addTab({
type: "terminal",
title: host.name || host.ip,
data: {
host,
initialCommand: "",
},
data: { host, initialCommand: "" },
});
// Clean URL to prevent re-opening on refresh
window.history.replaceState({}, "", "/");
} else {
toast.error(`Host "${hostIdentifier}" not found`);
}
} catch (error) {
console.error("Failed to open terminal for host:", error);
console.error("Failed to open terminal:", error);
toast.error("Failed to open terminal for host");
}
};
openTerminalForHost();
openTerminal();
}
}, [addTab]);