Skip to content

Commit 80391e7

Browse files
authored
Fix(desktop): recover from stale saved server URLs (#35)
* Fix(desktop): recover from stale saved server URLs * fix: update changelog for desktop saved server recovery and clean up SourcePreview component * fix: update changelog for desktop cross-platform builds and clean up imports in lib.rs
1 parent b44e9c2 commit 80391e7

8 files changed

Lines changed: 746 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.11.9] — 2026-06-01
9+
10+
### Added
11+
12+
- **File preview source view** — HTML and Markdown file previews now include a desktop Source toggle next to Download, showing the raw source with a direct copy action.
13+
14+
### Fixed
15+
16+
- **Desktop saved server recovery** — the Tauri desktop app now opens the bundled connect screen first, validates any saved Placet server before navigating, and shows a clean reconnect panel with retry/change actions when an old or API-only host URL cannot be opened.
17+
- **Desktop cross-platform builds** — Windows and Linux desktop builds no longer lose the shared URL-validation timeout import behind the macOS-only notification code path.
18+
819
## [0.11.8] — 2026-05-27
920

1021
### Fixed

apps/desktop/shell/connect.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
--input-bg: #ffffff;
99
--primary: #7c9cb5;
1010
--primary-fg: #ffffff;
11+
--info-bg: rgba(124, 156, 181, 0.12);
12+
--info-fg: #45687f;
1113
--error-bg: rgba(180, 35, 24, 0.1);
1214
--error-fg: #b42318;
1315
}
@@ -22,6 +24,8 @@
2224
--input-bg: #1a1a19;
2325
--primary: #8faec8;
2426
--primary-fg: #1a1a19;
27+
--info-bg: rgba(143, 174, 200, 0.14);
28+
--info-fg: #a9c7df;
2529
--error-bg: rgba(249, 112, 102, 0.12);
2630
--error-fg: #f97066;
2731
}
@@ -92,6 +96,48 @@ h1 {
9296
text-align: center;
9397
}
9498

99+
.saved-server {
100+
display: grid;
101+
gap: 12px;
102+
margin-bottom: 20px;
103+
padding: 14px;
104+
border: 1px solid color-mix(in srgb, var(--primary) 35%, var(--border));
105+
border-radius: 0.875rem;
106+
background: var(--info-bg);
107+
}
108+
109+
.saved-server[data-state='error'] {
110+
border-color: color-mix(in srgb, var(--error-fg) 35%, var(--border));
111+
background: var(--error-bg);
112+
}
113+
114+
.eyebrow {
115+
margin: 0 0 4px;
116+
color: var(--info-fg);
117+
font-size: 11px;
118+
font-weight: 700;
119+
letter-spacing: 0.08em;
120+
text-transform: uppercase;
121+
}
122+
123+
.saved-server[data-state='error'] .eyebrow {
124+
color: var(--error-fg);
125+
}
126+
127+
.saved-url {
128+
margin: 0;
129+
color: var(--fg);
130+
font-size: 13px;
131+
line-height: 1.45;
132+
overflow-wrap: anywhere;
133+
}
134+
135+
.saved-actions {
136+
display: grid;
137+
grid-template-columns: 1fr 1fr;
138+
gap: 8px;
139+
}
140+
95141
form {
96142
display: flex;
97143
flex-direction: column;
@@ -188,6 +234,26 @@ button:disabled {
188234
cursor: progress;
189235
}
190236

237+
button.secondary,
238+
button.ghost {
239+
height: 36px;
240+
border: 1px solid var(--border);
241+
background: var(--card);
242+
color: var(--fg);
243+
font-weight: 600;
244+
}
245+
246+
button.ghost {
247+
background: transparent;
248+
color: var(--muted);
249+
}
250+
251+
button.secondary:hover:not(:disabled),
252+
button.ghost:hover:not(:disabled) {
253+
border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
254+
opacity: 1;
255+
}
256+
191257
.error {
192258
margin: 0;
193259
padding: 10px 14px;

apps/desktop/shell/connect.js

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// navigates the webview to that origin. The rest of the app is the
55
// regular Placet web frontend served by the user's backend.
66

7+
const { invoke } = window.__TAURI__.core;
78
const { load } = window.__TAURI__.store;
89

910
const form = document.getElementById('connect-form');
@@ -12,6 +13,11 @@ const apiInput = document.getElementById('api-url');
1213
const advanced = document.querySelector('details.advanced');
1314
const button = document.getElementById('submit');
1415
const errorEl = document.getElementById('error');
16+
const subtitle = document.querySelector('.subtitle');
17+
const savedServer = document.getElementById('saved-server');
18+
const savedServerUrl = document.getElementById('saved-server-url');
19+
const retrySaved = document.getElementById('retry-saved');
20+
const changeServer = document.getElementById('change-server');
1521

1622
function showError(message) {
1723
errorEl.textContent = message;
@@ -23,6 +29,23 @@ function clearError() {
2329
errorEl.textContent = '';
2430
}
2531

32+
function setBusy(isBusy, label = 'Connect') {
33+
button.disabled = isBusy;
34+
button.textContent = isBusy ? label : 'Connect';
35+
if (retrySaved) retrySaved.disabled = isBusy;
36+
}
37+
38+
function showSavedServer(baseUrl, state = 'checking') {
39+
if (!savedServer || !savedServerUrl) return;
40+
savedServer.dataset.state = state;
41+
savedServer.hidden = false;
42+
savedServerUrl.textContent = baseUrl;
43+
}
44+
45+
function hideSavedServer() {
46+
if (savedServer) savedServer.hidden = true;
47+
}
48+
2649
function normalize(rawUrl) {
2750
const trimmed = rawUrl.trim().replace(/\/+$/, '');
2851
if (!/^https?:\/\//i.test(trimmed)) {
@@ -34,19 +57,41 @@ function normalize(rawUrl) {
3457
return trimmed;
3558
}
3659

37-
async function probe(baseUrl) {
38-
// Lightweight reachability check. We don't require a specific
39-
// endpoint to exist — any 2xx/3xx/4xx response proves the host is up.
40-
const controller = new AbortController();
41-
const timeout = setTimeout(() => controller.abort(), 5000);
60+
async function validate(baseUrl, apiUrl) {
61+
return invoke('validate_server_url', { baseUrl, apiUrl });
62+
}
63+
64+
async function saveAndOpen(baseUrl, apiUrl) {
65+
const store = await load('placet.json', { autoSave: true });
66+
await store.set('baseUrl', baseUrl);
67+
if (apiUrl) {
68+
await store.set('apiUrl', apiUrl);
69+
} else {
70+
await store.delete('apiUrl');
71+
}
72+
await store.save();
73+
74+
window.location.replace(baseUrl);
75+
}
76+
77+
async function connectTo(baseUrl, apiUrl, { saved = false } = {}) {
78+
setBusy(true, saved ? 'Checking…' : 'Connecting…');
79+
clearError();
80+
4281
try {
43-
await fetch(`${baseUrl}/api/auth/me`, {
44-
method: 'GET',
45-
credentials: 'include',
46-
signal: controller.signal,
47-
});
82+
const result = await validate(baseUrl, apiUrl || null);
83+
if (saved) showSavedServer(result.base_url, 'connected');
84+
await saveAndOpen(result.base_url, result.api_url || null);
85+
} catch (err) {
86+
if (saved) {
87+
showSavedServer(baseUrl, 'error');
88+
if (subtitle)
89+
subtitle.textContent =
90+
'The saved Placet server could not be opened. Choose a different host or update the URL below.';
91+
}
92+
showError(err instanceof Error ? err.message : String(err));
4893
} finally {
49-
clearTimeout(timeout);
94+
setBusy(false);
5095
}
5196
}
5297

@@ -66,34 +111,25 @@ form.addEventListener('submit', async (event) => {
66111
return;
67112
}
68113

69-
button.disabled = true;
70-
button.textContent = 'Connecting…';
71-
72-
try {
73-
try {
74-
await probe(apiUrl ?? baseUrl);
75-
} catch (err) {
76-
showError(`Could not reach ${apiUrl ?? baseUrl}. Check the URL and try again.`);
77-
return;
78-
}
114+
hideSavedServer();
115+
await connectTo(baseUrl, apiUrl);
116+
});
79117

80-
const store = await load('placet.json', { autoSave: true });
81-
await store.set('baseUrl', baseUrl);
82-
if (apiUrl) {
83-
await store.set('apiUrl', apiUrl);
84-
} else {
85-
await store.delete('apiUrl');
86-
}
87-
await store.save();
118+
retrySaved?.addEventListener('click', async () => {
119+
clearError();
120+
const baseUrl = input.value.trim();
121+
const apiUrl = apiInput?.value.trim() || null;
122+
if (baseUrl) await connectTo(baseUrl, apiUrl, { saved: true });
123+
});
88124

89-
window.location.replace(baseUrl);
90-
} finally {
91-
button.disabled = false;
92-
button.textContent = 'Connect';
93-
}
125+
changeServer?.addEventListener('click', () => {
126+
hideSavedServer();
127+
clearError();
128+
input.focus();
129+
input.select();
94130
});
95131

96-
// Pre-fill if a URL was already saved (e.g. user invoked "Switch server").
132+
// Pre-fill and validate a saved URL before opening it.
97133
(async () => {
98134
try {
99135
const store = await load('placet.json', { autoSave: true });
@@ -104,6 +140,13 @@ form.addEventListener('submit', async (event) => {
104140
apiInput.value = existingApi;
105141
if (advanced) advanced.open = true;
106142
}
143+
144+
if (typeof existing === 'string' && existing.trim() !== '') {
145+
showSavedServer(existing, 'checking');
146+
await connectTo(existing, typeof existingApi === 'string' ? existingApi : null, {
147+
saved: true,
148+
});
149+
}
107150
} catch {
108151
/* first run — nothing to load */
109152
}

apps/desktop/shell/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828
<h1>Connect to Placet</h1>
2929
<p class="subtitle">Enter the URL of your Placet server to get started.</p>
3030

31+
<section id="saved-server" class="saved-server" hidden aria-live="polite">
32+
<div>
33+
<p class="eyebrow">Saved server</p>
34+
<p id="saved-server-url" class="saved-url"></p>
35+
</div>
36+
<div class="saved-actions">
37+
<button id="retry-saved" type="button" class="secondary">Retry</button>
38+
<button id="change-server" type="button" class="ghost">Change</button>
39+
</div>
40+
</section>
41+
3142
<form id="connect-form" novalidate>
3243
<p id="error" class="error" hidden></p>
3344

0 commit comments

Comments
 (0)