polish(desktop): non-destructive Change Instance + recovery enter/exit logs
UX bug found during smoke testing: clicking Change Instance immediately deleted the saved instance URL and showed an empty picker, with no way back if the user changed their mind. Fix: - Don't clearInstanceUrl() in recovery action 'change-instance' — picker is now non-destructive - Picker pre-fills the input with the current saved URL when present - Cancel button (shown only when a saved URL exists) returns to current instance via idempotent setInstanceUrl re-save - Header copy switches to 'Switch instance' / 'Cancel to stay' framing when a saved URL is present - URL only overwrites on explicit Connect to a different instance Also: add console.log enter/exit lines in enterRecoveryMode and the clear-recovery-state action handlers, so smoke-test scripts can grep stderr for recovery activity without UI introspection. Spec + docs/systems/desktop.md updated.
This commit is contained in:
@@ -136,6 +136,29 @@
|
||||
}
|
||||
|
||||
/* ── Button — accent-primary with white text (matches login) ── */
|
||||
.cancel-btn {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
background: transparent;
|
||||
color: #a0a0aa;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.cancel-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: #efefef;
|
||||
}
|
||||
.cancel-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.connect-btn {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
@@ -264,6 +287,7 @@
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="connect-btn" id="connect-btn">Connect</button>
|
||||
<button type="button" class="cancel-btn" id="cancel-btn" style="display: none;">Cancel</button>
|
||||
</form>
|
||||
|
||||
<div id="instance-info" class="instance-info" style="display: none;">
|
||||
@@ -289,6 +313,8 @@
|
||||
const infoEl = document.getElementById('instance-info');
|
||||
const nameEl = document.getElementById('instance-name');
|
||||
const versionEl = document.getElementById('instance-version');
|
||||
const cancelBtn = document.getElementById('cancel-btn');
|
||||
let savedUrl = null;
|
||||
|
||||
function normalizeUrl(raw) {
|
||||
let url = raw.trim();
|
||||
@@ -315,12 +341,39 @@
|
||||
function setLoading(loading) {
|
||||
input.disabled = loading;
|
||||
btn.disabled = loading;
|
||||
cancelBtn.disabled = loading;
|
||||
loadingEl.style.display = loading ? 'block' : 'none';
|
||||
if (loading) {
|
||||
hideError();
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (window.backspace && window.backspace.getInstanceUrl) {
|
||||
try {
|
||||
savedUrl = await window.backspace.getInstanceUrl();
|
||||
} catch { savedUrl = null; }
|
||||
}
|
||||
if (savedUrl) {
|
||||
input.value = savedUrl;
|
||||
cancelBtn.style.display = 'block';
|
||||
// Update header copy to reflect the "switch" intent rather than the
|
||||
// first-run "welcome" framing.
|
||||
document.querySelector('.header h1').textContent = 'Switch instance';
|
||||
document.querySelector('.header p').textContent = 'Connect to a different Backspace instance, or cancel to stay.';
|
||||
}
|
||||
}
|
||||
init();
|
||||
|
||||
cancelBtn.addEventListener('click', async () => {
|
||||
if (!savedUrl) return;
|
||||
cancelBtn.disabled = true;
|
||||
btn.disabled = true;
|
||||
if (window.backspace && window.backspace.setInstanceUrl) {
|
||||
await window.backspace.setInstanceUrl(savedUrl);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
@@ -298,6 +298,7 @@ export function setOnQuitRequested(cb: (() => void) | null): void {
|
||||
|
||||
export function enterRecoveryMode(reason: { code: RecoveryReasonCode; detail: string }): void {
|
||||
recoveryStore.update({ mode: 'recovery', reason });
|
||||
console.log(`[recovery] entered: ${reason.code} — ${reason.detail}`);
|
||||
|
||||
if (recoveryStore.isInRecoveryMode()) {
|
||||
// Already in recovery — state.reason updated for display, no re-navigation.
|
||||
@@ -361,6 +362,7 @@ export function handleRecoveryAction(action: RecoveryAction): void {
|
||||
// fails, did-fail-load re-enters recovery. If it stalls, boot timer fires.
|
||||
recoveryStore.markRecoveryExited();
|
||||
recoveryStore.update({ mode: 'normal', reason: null });
|
||||
console.log('[recovery] exited (reload)');
|
||||
if (!url) {
|
||||
mainWindowRef?.loadFile(getPickerPath());
|
||||
return;
|
||||
@@ -385,9 +387,13 @@ export function handleRecoveryAction(action: RecoveryAction): void {
|
||||
return;
|
||||
}
|
||||
case 'change-instance': {
|
||||
clearInstanceUrl();
|
||||
// Non-destructive: don't clear the saved URL here. The picker pre-fills it
|
||||
// and offers Cancel — the URL is only overwritten when the user explicitly
|
||||
// Connects to a different one. (clearInstanceUrl IPC remains for explicit
|
||||
// "disconnect" operations from the web settings UI.)
|
||||
recoveryStore.markRecoveryExited();
|
||||
recoveryStore.update({ mode: 'normal', reason: null });
|
||||
console.log('[recovery] exited (change-instance)');
|
||||
mainWindowRef?.loadFile(getPickerPath());
|
||||
// Ensure visible — tray clicks may happen with window hidden, and the
|
||||
// recovery surface should also remain visible during the navigation.
|
||||
|
||||
Reference in New Issue
Block a user