Implement connection flow, test connection, and secret prompts

- Change ConnectSession/ConnectQuick to return SessionInfo (not string)
- Change UpdateProfile to return (Profile, error) for frontend use
- Make session connection asynchronous to avoid Wails event loop deadlock
- Implement event-based secret prompt (backend ↔ frontend via requestID)
- Add TestConnection API for TCP reachability checks with latency
- Add 'Save & Connect' button in profile editor
- Wire _testConnection to real backend TCP check
- Add btn-accent CSS for the green action button
This commit is contained in:
2026-04-15 00:00:00 +01:00
parent ecb46cda6f
commit 8752ecef2d
7 changed files with 240 additions and 54 deletions
+46 -28
View File
@@ -94,43 +94,48 @@ func (m *Manager) Connect(profile *models.Profile, getSecret func(prompt string)
"state": string(models.SessionConnecting),
})
var err error
switch profile.Protocol {
case models.ProtocolSSH:
err = m.connectSSH(sess, getSecret)
case models.ProtocolTelnet:
err = m.connectTelnet(sess)
case models.ProtocolRAW:
err = m.connectRAW(sess)
default:
err = fmt.Errorf("unsupported protocol %q", profile.Protocol)
}
// Perform the actual protocol handshake in a goroutine so we do not
// block the Wails event loop — secret prompts require the UI thread.
go func() {
var err error
switch profile.Protocol {
case models.ProtocolSSH:
err = m.connectSSH(sess, getSecret)
case models.ProtocolTelnet:
err = m.connectTelnet(sess)
case models.ProtocolRAW:
err = m.connectRAW(sess)
default:
err = fmt.Errorf("unsupported protocol %q", profile.Protocol)
}
if err != nil {
m.mu.Lock()
sess.Info.State = models.SessionError
sess.Info.Error = err.Error()
m.mu.Unlock()
m.emitEvent("session:state", map[string]interface{}{
"sessionID": sessionID,
"state": string(models.SessionError),
"error": err.Error(),
})
return
}
if err != nil {
m.mu.Lock()
sess.Info.State = models.SessionError
sess.Info.Error = err.Error()
sess.Info.State = models.SessionConnected
sess.Info.ConnectedAt = time.Now()
m.mu.Unlock()
m.emitEvent("session:state", map[string]interface{}{
"sessionID": sessionID,
"state": string(models.SessionError),
"error": err.Error(),
"state": string(models.SessionConnected),
})
return "", fmt.Errorf("sessions: connection failed: %w", err)
}
m.mu.Lock()
sess.Info.State = models.SessionConnected
sess.Info.ConnectedAt = time.Now()
m.mu.Unlock()
m.auditLog.LogConnect(profile.ID, sessionID, string(profile.Protocol), profile.Host)
}()
m.emitEvent("session:state", map[string]interface{}{
"sessionID": sessionID,
"state": string(models.SessionConnected),
})
m.auditLog.LogConnect(profile.ID, sessionID, string(profile.Protocol), profile.Host)
return sessionID, nil
}
@@ -240,6 +245,19 @@ func (m *Manager) GetSession(sessionID string) (*models.SessionInfo, error) {
return &info, nil
}
// GetSessionInfo returns session metadata or nil if the session does not exist.
func (m *Manager) GetSessionInfo(sessionID string) *models.SessionInfo {
m.mu.RLock()
defer m.mu.RUnlock()
sess, ok := m.sessions[sessionID]
if !ok {
return nil
}
info := sess.Info
return &info
}
// ListSessions returns all active session info.
func (m *Manager) ListSessions() []models.SessionInfo {
m.mu.RLock()