diff --git a/package-lock.json b/package-lock.json index 1c9d6013..b4b66d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@tailwindcss/vite": "^4.1.14", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.9", + "@types/cytoscape": "^3.21.9", "@types/jszip": "^3.4.0", "@types/multer": "^2.0.0", "@types/qrcode": "^1.5.5", @@ -56,6 +57,7 @@ "cmdk": "^1.1.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "cytoscape": "^3.33.1", "dotenv": "^17.2.0", "drizzle-orm": "^0.44.3", "express": "^5.1.0", @@ -72,6 +74,7 @@ "node-fetch": "^3.3.2", "qrcode": "^1.5.4", "react": "^19.1.0", + "react-cytoscapejs": "^2.0.0", "react-dom": "^19.1.0", "react-h5-audio-player": "^3.10.1", "react-hook-form": "^7.60.0", @@ -5228,6 +5231,12 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.9", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", + "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -7776,6 +7785,15 @@ "integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -15109,6 +15127,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-cytoscapejs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-cytoscapejs/-/react-cytoscapejs-2.0.0.tgz", + "integrity": "sha512-t3SSl1DQy7+JQjN+8QHi1anEJlM3i3aAeydHTsJwmjo/isyKK7Rs7oCvU6kZsB9NwZidzZQR21Vm2PcBLG/Tjg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "cytoscape": "^3.2.19", + "react": ">=15.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 463a2cb3..f38ad243 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -671,6 +671,13 @@ const migrateSchema = () => { `); } catch (createError) { databaseLogger.warn("Failed to create network_topology table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { sqlite.prepare("SELECT id FROM host_access LIMIT 1").get(); } catch { try { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index b3e0c327..70393d47 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -301,6 +301,14 @@ export const networkTopology = sqliteTable("network_topology", { .notNull() .references(() => users.id, { onDelete: "cascade" }), topology: text("topology"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + export const hostAccess = sqliteTable("host_access", { id: integer("id").primaryKey({ autoIncrement: true }), hostId: integer("host_id") diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index f9e20f8a..c0b7c11a 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -1803,6 +1803,10 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ } catch (e) { statsLogger.debug("Failed to collect ports metrics", { operation: "ports_metrics_failed", + error: e instanceof Error ? e.message : String(e), + }); + } + let firewall: { type: "iptables" | "nftables" | "none"; status: "active" | "inactive" | "unknown"; diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts index ab161e2c..c0842f58 100644 --- a/src/backend/utils/user-data-import.ts +++ b/src/backend/utils/user-data-import.ts @@ -194,7 +194,7 @@ class UserDataImport { continue; } - const newHostData = { + const newHostData: Record = { ...host, userId: targetUserId, updatedAt: new Date().toISOString(), @@ -204,7 +204,7 @@ class UserDataImport { newHostData.createdAt = new Date().toISOString(); } - let processedHostData = newHostData; + let processedHostData: Record = newHostData; if (options.userDataKey) { processedHostData = DataCrypto.encryptRecord( "ssh_data", @@ -275,7 +275,7 @@ class UserDataImport { continue; } - const newCredentialData = { + const newCredentialData: Record = { ...credential, userId: targetUserId, updatedAt: new Date().toISOString(), @@ -287,7 +287,7 @@ class UserDataImport { newCredentialData.createdAt = new Date().toISOString(); } - let processedCredentialData = newCredentialData; + let processedCredentialData: Record = newCredentialData; if (options.userDataKey) { processedCredentialData = DataCrypto.encryptRecord( "ssh_credentials", diff --git a/src/locales/en.json b/src/locales/en.json index 9781e412..bf3c0ba1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1740,6 +1740,7 @@ "state": "State", "process": "Process", "noData": "No listening ports data" + }, "firewall": { "title": "Firewall", "active": "Active", diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts index 31477eb2..f8e18fc0 100644 --- a/src/types/stats-widgets.ts +++ b/src/types/stats-widgets.ts @@ -7,7 +7,8 @@ export type WidgetType = | "processes" | "system" | "login_stats" - | "ports"; + | "ports" + | "firewall"; export interface ListeningPort { protocol: "tcp" | "udp"; @@ -21,7 +22,7 @@ export interface ListeningPort { export interface PortsMetrics { source: "ss" | "netstat" | "none"; ports: ListeningPort[]; - | "firewall"; +} export interface FirewallRule { chain: string; diff --git a/src/ui/desktop/apps/HostManagerApp.tsx b/src/ui/desktop/apps/HostManagerApp.tsx index fce7e0df..6feb771b 100644 --- a/src/ui/desktop/apps/HostManagerApp.tsx +++ b/src/ui/desktop/apps/HostManagerApp.tsx @@ -1,4 +1,4 @@ -import { HostManager } from "@/ui/desktop/apps/host-manager/HostManager"; +import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager"; import React from "react"; const HostManagerApp: React.FC = () => { diff --git a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx index 20ab7472..48f9f654 100644 --- a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx +++ b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx @@ -270,7 +270,8 @@ export function ServerStats({ case "ports": return ( - + ); + case "firewall": return ( diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index 5fcb0618..775aef16 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -361,6 +361,7 @@ export function AppView({ rightSidebarOpen={rightSidebarOpen} rightSidebarWidth={rightSidebarWidth} isStandalone={true} + /> ) : t.type === "tunnel" ? (