Files
nexus/internal/app/app.go
T
2026-06-16 15:43:25 +01:00

168 lines
5.0 KiB
Go

// Package app provides the core application lifecycle for Nexus Terminal.
// It wires together storage, secrets, sessions, preferences, known hosts,
// and the audit log, and exposes Wails lifecycle hooks.
package app
import (
"context"
"log"
"path/filepath"
"sync"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/andydixon/nexus/internal/api"
"github.com/andydixon/nexus/internal/audit"
"github.com/andydixon/nexus/internal/config"
"github.com/andydixon/nexus/internal/knownhosts"
"github.com/andydixon/nexus/internal/preferences"
"github.com/andydixon/nexus/internal/recording"
"github.com/andydixon/nexus/internal/secrets"
"github.com/andydixon/nexus/internal/sessions"
"github.com/andydixon/nexus/internal/storage"
)
// App is the central application struct. Wails binds its lifecycle methods
// (Startup, Shutdown, DomReady) and it holds references to every subsystem.
type App struct {
ctx context.Context
mu sync.Mutex
Config *config.AppConfig
Store *storage.Store
SecretStore *secrets.SecretStore
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. The API service is pre-created
// so it can be bound by Wails before Startup runs.
func NewApp() *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
// configuration, ensures required directories exist, and initialises all
// subsystems (storage, secret store, session manager, preferences).
func (a *App) Startup(ctx context.Context) {
a.mu.Lock()
defer a.mu.Unlock()
a.ctx = ctx
// Load platform-specific configuration paths.
cfg, err := config.Load()
if err != nil {
log.Fatalf("failed to load configuration: %v", err)
}
a.Config = cfg
if err := cfg.EnsureDirectories(); err != nil {
log.Fatalf("failed to create application directories: %v", err)
}
// 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 session recording store.
recordDir := filepath.Join(cfg.DataDir, "recordings")
recordStore := recording.NewStore(recordDir)
// Initialise the session manager.
a.SessionManager = sessions.NewManager(store, a.SecretStore, a.KeychainStore, a.AuditLog, recordStore)
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)
}
// Shutdown is called by Wails when the application is closing. It ensures all
// sessions are cleanly terminated and persistent storage is flushed.
func (a *App) Shutdown(ctx context.Context) {
a.mu.Lock()
defer a.mu.Unlock()
if a.SessionManager != nil {
a.SessionManager.CloseAll()
}
if a.Store != nil {
if err := a.Store.Close(); err != nil {
log.Printf("warning: error closing storage: %v", err)
}
}
// Lock the secret store to zero key material.
if a.SecretStore != nil {
a.SecretStore.Lock()
}
}
// DomReady is called by Wails once the frontend DOM is fully loaded. It emits
// a "backend:ready" event so the frontend knows the backend is prepared.
func (a *App) DomReady(ctx context.Context) {
runtime.EventsEmit(ctx, "backend:ready", map[string]interface{}{
"message": "Backend is ready",
})
}
// EmitOpenAbout tells the frontend to open the About tab.
func (a *App) EmitOpenAbout() {
a.mu.Lock()
ctx := a.ctx
a.mu.Unlock()
if ctx == nil {
log.Println("unable to open About: application context not initialised")
return
}
runtime.EventsEmit(ctx, "app:open-about", nil)
}
// RequestQuit asks the runtime to quit the application.
func (a *App) RequestQuit() {
a.mu.Lock()
ctx := a.ctx
a.mu.Unlock()
if ctx == nil {
log.Println("unable to quit: application context not initialised")
return
}
runtime.Quit(ctx)
}