feat: complete Nexus Terminal — premium SSH/Telnet/RAW TCP desktop client

Production-minded Wails v2 desktop application with:
- Full SSH client (password, key, keyboard-interactive, jump hosts, keepalive)
- Telnet client (RFC 854/855 negotiation, NAWS, TTYPE, encoding)
- RAW TCP client (binary-safe, text/hex/split views, payload presets)
- SQLite storage with migrations and repository abstraction
- Encrypted secret storage (AES-256-GCM + Argon2id) with keychain fallback
- Known hosts verification (strict/ask/accept-new/insecure)
- Session manager with multi-session lifecycle and Wails events
- Dark charcoal Material-inspired design system (CSS custom properties)
- Tabbed session management with rename, reorder, pin, context menu
- Profile editor with protocol-aware form sections
- Command palette, notifications, settings modal
- JSON import/export, audit logging, preferences
- 50+ tests across validation, secrets, storage, encoding, sessions
- Makefile, build.sh, .gitignore, comprehensive README
This commit is contained in:
2026-04-15 00:00:00 +01:00
parent c767c1640e
commit 5bff1d9f3e
17 changed files with 5644 additions and 45 deletions
+60 -35
View File
@@ -5,35 +5,21 @@ package app
import (
"context"
"fmt"
"log"
"sync"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/copilot-ssh-client/app/internal/api"
"github.com/copilot-ssh-client/app/internal/audit"
"github.com/copilot-ssh-client/app/internal/config"
"github.com/copilot-ssh-client/app/internal/knownhosts"
"github.com/copilot-ssh-client/app/internal/preferences"
"github.com/copilot-ssh-client/app/internal/secrets"
"github.com/copilot-ssh-client/app/internal/sessions"
"github.com/copilot-ssh-client/app/internal/storage"
)
// Storage abstracts the persistent data layer (SQLite).
type Storage interface {
Close() error
}
// SessionManager tracks active connection sessions.
type SessionManager interface {
CloseAll()
}
// Preferences manages user application preferences.
type Preferences interface{}
// KnownHostStore manages trusted SSH host key records.
type KnownHostStore interface{}
// AuditLog records auditable application events.
type AuditLog interface{}
// App is the central application struct. Wails binds its lifecycle methods
// (Startup, Shutdown, DomReady) and it holds references to every subsystem.
type App struct {
@@ -41,18 +27,31 @@ type App struct {
mu sync.Mutex
Config *config.AppConfig
Storage Storage
Store *storage.Store
SecretStore *secrets.SecretStore
SessionManager SessionManager
Preferences Preferences
KnownHostStore KnownHostStore
AuditLog AuditLog
KeychainStore *secrets.KeychainStore
SessionManager *sessions.Manager
Prefs *preferences.Manager
KnownHosts *knownhosts.Store
AuditLog *audit.Logger
Service *api.Service
}
// NewApp creates a new App instance. Subsystems are not initialised until
// Startup is called by the Wails runtime.
// Startup is called by the Wails runtime. The API service is pre-created
// so it can be bound by Wails before Startup runs.
func NewApp() *App {
return &App{}
a := &App{}
// The API service is created eagerly so Wails can bind it. Its backing
// stores are nil at this point; they are wired during Startup.
a.Service = &api.Service{}
return a
}
// APIService returns the Wails-bound API service instance. This must be
// called before wails.Run so the binding is registered.
func (a *App) APIService() *api.Service {
return a.Service
}
// Startup is called by Wails when the application starts. It loads
@@ -64,6 +63,7 @@ func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
// Load platform-specific configuration paths.
cfg, err := config.Load()
if err != nil {
log.Fatalf("failed to load configuration: %v", err)
@@ -74,12 +74,37 @@ func (a *App) Startup(ctx context.Context) {
log.Fatalf("failed to create application directories: %v", err)
}
// Initialise the encrypted secret store.
a.SecretStore = secrets.NewSecretStore(cfg.SecretPath)
// Initialise SQLite storage (migrations run automatically on open).
store, err := storage.NewStore(cfg.DBPath)
if err != nil {
log.Fatalf("failed to open storage: %v", err)
}
a.Store = store
// Initialise secret stores (keychain preferred, encrypted file fallback).
a.SecretStore = secrets.NewSecretStore(cfg.SecretPath)
a.KeychainStore = secrets.NewKeychainStore("NexusTerminal")
// Initialise the audit logger backed by SQLite.
a.AuditLog = audit.NewLogger(store)
// Initialise known hosts store.
a.KnownHosts = knownhosts.NewStore(store)
// Initialise preferences manager.
a.Prefs = preferences.NewManager(store)
if err := a.Prefs.Load(); err != nil {
log.Printf("warning: failed to load preferences, using defaults: %v", err)
}
// Initialise the session manager.
a.SessionManager = sessions.NewManager(store, a.SecretStore, a.KeychainStore, a.AuditLog)
a.SessionManager.SetContext(ctx)
// Wire the API service with all backing stores now that they exist.
*a.Service = *api.NewService(store, a.SecretStore, a.KeychainStore, a.SessionManager, a.AuditLog, a.Prefs)
a.Service.SetContext(ctx)
// Storage, session manager, preferences, known host store and audit log
// will be wired in when their implementations are available.
// For now we log successful initialisation.
log.Printf("Nexus Terminal initialised – data directory: %s", cfg.DataDir)
}
@@ -93,8 +118,8 @@ func (a *App) Shutdown(ctx context.Context) {
a.SessionManager.CloseAll()
}
if a.Storage != nil {
if err := a.Storage.Close(); err != nil {
if a.Store != nil {
if err := a.Store.Close(); err != nil {
log.Printf("warning: error closing storage: %v", err)
}
}
@@ -113,5 +138,5 @@ func (a *App) DomReady(ctx context.Context) {
runtime.EventsEmit(ctx, "backend:ready", map[string]interface{}{
"message": "Backend is ready",
})
fmt.Println("DOM ready – backend:ready event emitted")
log.Println("DOM ready – backend:ready event emitted")
}