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
+15
View File
@@ -50,6 +50,21 @@
border-color: #3da69f;
}
/* Accent — success-tinted action, e.g. "Save & Connect" */
.btn-accent {
background: var(--success);
color: var(--text-inverse);
border-color: var(--success);
}
.btn-accent:hover {
background: #57a85c;
border-color: #57a85c;
}
.btn-accent:active {
background: #4a9a4e;
border-color: #4a9a4e;
}
/* Secondary — subtle border */
.btn-secondary {
background: transparent;
+2 -2
View File
@@ -185,12 +185,12 @@ class App {
payload.message || 'Enter your password or passphrase.',
);
window.runtime.EventsEmit('session:secret:response', {
sessionID: payload.sessionID,
requestID: payload.requestID,
value,
});
} catch (_) {
window.runtime.EventsEmit('session:secret:response', {
sessionID: payload.sessionID,
requestID: payload.requestID,
value: '',
cancelled: true,
});
+23 -6
View File
@@ -163,16 +163,19 @@ class ProfileEditor {
// ── Footer ──────────────────────────────────────────────
const footer = createElement('div', 'drawer-footer');
const cancelBtn = createElement('button', 'btn btn-secondary', { textContent: 'Cancel' });
const testBtn = createElement('button', 'btn btn-outline', { textContent: 'Test Connection' });
const testBtn = createElement('button', 'btn btn-outline', { textContent: 'Test' });
const saveBtn = createElement('button', 'btn btn-primary', { textContent: isNew ? 'Create' : 'Save' });
const connectBtn = createElement('button', 'btn btn-accent', { textContent: isNew ? 'Create & Connect' : 'Save & Connect' });
cancelBtn.addEventListener('click', () => this.close());
testBtn.addEventListener('click', () => this._testConnection());
saveBtn.addEventListener('click', () => this._handleSave());
saveBtn.addEventListener('click', () => this._handleSave(false));
connectBtn.addEventListener('click', () => this._handleSave(true));
footer.appendChild(cancelBtn);
footer.appendChild(testBtn);
footer.appendChild(saveBtn);
footer.appendChild(connectBtn);
drawer.appendChild(footer);
// ── Events ──────────────────────────────────────────────
@@ -807,7 +810,7 @@ class ProfileEditor {
// ── Actions ──────────────────────────────────────────────────────
/** @private */
async _handleSave() {
async _handleSave(andConnect = false) {
this._collectFormValues();
const errors = this._validate(this._profile);
if (errors.length > 0) {
@@ -816,17 +819,22 @@ class ProfileEditor {
}
try {
let saved;
if (this._isNew) {
const saved = await window.go.api.Service.CreateProfile(this._profile);
saved = await window.go.api.Service.CreateProfile(this._profile);
this.app.state.addProfile(saved);
this.app.notifications.success('Profile created');
} else {
const saved = await window.go.api.Service.UpdateProfile(this._profile);
saved = await window.go.api.Service.UpdateProfile(this._profile);
this.app.state.updateProfile(saved);
this.app.notifications.success('Profile updated');
}
this.app.sidebar.render();
this.close();
if (andConnect && saved && saved.id) {
this.app.connectProfile(saved.id);
}
} catch (err) {
this.app.notifications.error('Save failed: ' + err);
}
@@ -841,7 +849,16 @@ class ProfileEditor {
return;
}
this.app.notifications.info('Test connection is not yet available.');
const host = this._profile.host;
const port = this._profile.port;
this.app.notifications.info(`Testing connection to ${host}:${port}…`);
try {
const result = await window.go.api.Service.TestConnection(host, port);
this.app.notifications.success(`Reachable — ${result.latencyMs}ms latency`);
} catch (err) {
this.app.notifications.error('Test failed: ' + err);
}
}
// ── Utilities ────────────────────────────────────────────────────
+7 -3
View File
@@ -7,9 +7,9 @@ export function ClearSessionHistory(arg1:string):Promise<void>;
export function CloseAllSessions():Promise<void>;
export function ConnectQuick(arg1:models.Profile):Promise<string>;
export function ConnectQuick(arg1:models.Profile):Promise<models.SessionInfo>;
export function ConnectSession(arg1:string):Promise<string>;
export function ConnectSession(arg1:string):Promise<models.SessionInfo>;
export function CreateProfile(arg1:models.Profile):Promise<models.Profile>;
@@ -65,6 +65,8 @@ export function PinSession(arg1:string,arg2:boolean):Promise<void>;
export function ReconnectSession(arg1:string):Promise<void>;
export function RegisterSecretResponseListener():Promise<void>;
export function RenameSession(arg1:string,arg2:string):Promise<void>;
export function ResizeSession(arg1:string,arg2:number,arg3:number):Promise<void>;
@@ -87,9 +89,11 @@ export function SetFavourite(arg1:string,arg2:boolean):Promise<void>;
export function SetProfileSecret(arg1:string,arg2:string,arg3:string):Promise<void>;
export function TestConnection(arg1:string,arg2:number):Promise<Record<string, any>>;
export function UnlockSecretStore(arg1:string):Promise<void>;
export function UpdateProfile(arg1:models.Profile):Promise<void>;
export function UpdateProfile(arg1:models.Profile):Promise<models.Profile>;
export function WriteSession(arg1:string,arg2:string):Promise<void>;
+8
View File
@@ -126,6 +126,10 @@ export function ReconnectSession(arg1) {
return window['go']['api']['Service']['ReconnectSession'](arg1);
}
export function RegisterSecretResponseListener() {
return window['go']['api']['Service']['RegisterSecretResponseListener']();
}
export function RenameSession(arg1, arg2) {
return window['go']['api']['Service']['RenameSession'](arg1, arg2);
}
@@ -170,6 +174,10 @@ export function SetProfileSecret(arg1, arg2, arg3) {
return window['go']['api']['Service']['SetProfileSecret'](arg1, arg2, arg3);
}
export function TestConnection(arg1, arg2) {
return window['go']['api']['Service']['TestConnection'](arg1, arg2);
}
export function UnlockSecretStore(arg1) {
return window['go']['api']['Service']['UnlockSecretStore'](arg1);
}