diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8a11e8b..d7921a8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,10 +17,8 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-search": "^0.16.0", "@xterm/addon-web-links": "^0.12.0", - "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.2", - "naive-ui": "^2.40.0", "pinia": "^2.2.0", "vue": "^3.5.0", "vue-router": "^4.4.0" @@ -256,30 +254,6 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@css-render/plugin-bem": { - "version": "0.15.14", - "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", - "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", - "license": "MIT", - "peerDependencies": { - "css-render": "~0.15.14" - } - }, - "node_modules/@css-render/vue3-ssr": { - "version": "0.15.14", - "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", - "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.0.11" - } - }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -771,12 +745,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "license": "Apache-2.0" - }, "node_modules/@lezer/common": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", @@ -1552,21 +1520,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -1776,12 +1729,6 @@ "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", "license": "MIT" }, - "node_modules/@xterm/addon-webgl": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", - "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==", - "license": "MIT" - }, "node_modules/@xterm/xterm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", @@ -1798,12 +1745,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1842,47 +1783,12 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, - "node_modules/css-render": { - "version": "0.15.14", - "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", - "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "~0.8.0", - "csstype": "~3.0.5" - } - }, - "node_modules/css-render/node_modules/csstype": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", - "license": "MIT", - "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" - } - }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -1974,12 +1880,6 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, - "node_modules/evtd": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", - "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2030,15 +1930,6 @@ "he": "bin/he" } }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2322,18 +2213,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2366,38 +2245,6 @@ "dev": true, "license": "MIT" }, - "node_modules/naive-ui": { - "version": "2.44.1", - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz", - "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==", - "license": "MIT", - "dependencies": { - "@css-render/plugin-bem": "^0.15.14", - "@css-render/vue3-ssr": "^0.15.14", - "@types/lodash": "^4.17.20", - "@types/lodash-es": "^4.17.12", - "async-validator": "^4.2.5", - "css-render": "^0.15.14", - "csstype": "^3.1.3", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "evtd": "^0.2.4", - "highlight.js": "^11.8.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "seemly": "^0.3.10", - "treemate": "^0.3.11", - "vdirs": "^0.1.8", - "vooks": "^0.2.12", - "vueuc": "^0.4.65" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2537,12 +2384,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/seemly": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz", - "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", - "license": "MIT" - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2596,12 +2437,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/treemate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz", - "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2616,18 +2451,6 @@ "node": ">=14.17" } }, - "node_modules/vdirs": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz", - "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", - "license": "MIT", - "dependencies": { - "evtd": "^0.2.2" - }, - "peerDependencies": { - "vue": "^3.0.11" - } - }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -2703,18 +2526,6 @@ } } }, - "node_modules/vooks": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz", - "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", - "license": "MIT", - "dependencies": { - "evtd": "^0.2.2" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -2801,24 +2612,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/vueuc": { - "version": "0.4.65", - "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz", - "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", - "license": "MIT", - "dependencies": { - "@css-render/vue3-ssr": "^0.15.10", - "@juggle/resize-observer": "^3.3.1", - "css-render": "^0.15.10", - "evtd": "^0.2.4", - "seemly": "^0.3.6", - "vdirs": "^0.1.4", - "vooks": "^0.2.4" - }, - "peerDependencies": { - "vue": "^3.0.11" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index a63810b..7ce3171 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,10 +20,8 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-search": "^0.16.0", "@xterm/addon-web-links": "^0.12.0", - "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.2", - "naive-ui": "^2.40.0", "pinia": "^2.2.0", "vue": "^3.5.0", "vue-router": "^4.4.0" diff --git a/internal/app/app.go b/internal/app/app.go index 334adb3..177ffb2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -45,6 +45,7 @@ type WraithApp struct { Credentials *credentials.CredentialService AI *ai.AIService Updater *updater.UpdateService + Workspace *WorkspaceService oauthMgr *ai.OAuthManager wailsApp *application.App unlocked bool @@ -79,18 +80,31 @@ func New(version string) (*WraithApp, error) { themeSvc := theme.NewThemeService(database) sessionMgr := session.NewManager() pluginReg := plugin.NewRegistry() + pluginReg.RegisterImporter(&importer.MobaConfImporter{}) + + // Host key store — persists SSH host key fingerprints for TOFU verification + hostKeyStore := ssh.NewHostKeyStore(database) // SSH output handler — emits Wails events to the frontend. // The closure captures `app` (the WraithApp being built). The wailsApp // field is set after application.New() in main.go, but SSH sessions only // start after app.Run(), so wailsApp is always valid at call time. var app *WraithApp - sshSvc := ssh.NewSSHService(database, func(sessionID string, data []byte) { + sshOutputHandler := func(sessionID string, data []byte) { if app != nil && app.wailsApp != nil { // Base64 encode binary data for safe transport over Wails events app.wailsApp.Event.Emit("ssh:data:"+sessionID, base64.StdEncoding.EncodeToString(data)) } - }) + } + + // CWD handler — emits Wails events when the remote working directory changes + sshCWDHandler := func(sessionID string, path string) { + if app != nil && app.wailsApp != nil { + app.wailsApp.Event.Emit("ssh:cwd:"+sessionID, path) + } + } + + sshSvc := ssh.NewSSHService(database, hostKeyStore, sshOutputHandler, sshCWDHandler) sftpSvc := sftp.NewSFTPService() // RDP service with platform-aware backend factory. @@ -118,6 +132,17 @@ func New(version string) (*WraithApp, error) { } updaterSvc := updater.NewUpdateService(version) + workspaceSvc := NewWorkspaceService(settingsSvc) + + // Clear the clean shutdown flag on startup — it will be re-set on clean exit. + // If it wasn't set, the previous run crashed and the workspace can be restored. + wasClean := workspaceSvc.WasCleanShutdown() + if err := workspaceSvc.ClearCleanShutdown(); err != nil { + slog.Warn("failed to clear clean shutdown flag", "error", err) + } + if !wasClean { + slog.Info("previous shutdown was not clean — workspace restore available") + } app = &WraithApp{ db: database, @@ -131,6 +156,7 @@ func New(version string) (*WraithApp, error) { RDP: rdpSvc, AI: aiSvc, Updater: updaterSvc, + Workspace: workspaceSvc, oauthMgr: oauthMgr, } return app, nil @@ -337,6 +363,17 @@ func (a *WraithApp) ConnectSSH(connectionID int64, cols, rows int) (string, erro slog.Warn("failed to update last_connected", "error", err) } + // Register with session manager + if _, err := a.Sessions.Create(connectionID, "ssh"); err != nil { + slog.Warn("failed to register SSH session in manager", "error", err) + } else { + // Store the SSH session ID as the manager session ID for lookup + a.Sessions.SetState(sessionID, session.StateConnected) + } + + // Save workspace state after session change + a.saveWorkspaceState() + slog.Info("SSH session started", "sessionID", sessionID, "host", conn.Hostname, "user", username) return sessionID, nil } diff --git a/internal/db/migrations/003_connection_history.sql b/internal/db/migrations/003_connection_history.sql new file mode 100644 index 0000000..33265b6 --- /dev/null +++ b/internal/db/migrations/003_connection_history.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS connection_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + connection_id INTEGER NOT NULL REFERENCES connections(id) ON DELETE CASCADE, + protocol TEXT NOT NULL, + connected_at DATETIME DEFAULT CURRENT_TIMESTAMP, + disconnected_at DATETIME, + duration_secs INTEGER +);