Implement hashtag url storage for easy retry

This commit is contained in:
2026-08-03 23:03:18 +02:00
parent 0d0da28660
commit 59b5f2dde6
2 changed files with 45 additions and 0 deletions
+5
View File
@@ -28,6 +28,11 @@ PORT=8090 ./build/immich-sync
Open `http://localhost:8090`. Open `http://localhost:8090`.
After both share URLs are valid, starting a job updates the browser fragment in the form
`#left=<encoded-share-url>&right=<encoded-share-url>`. Reloading or bookmarking that URL restores both fields and
automatically starts a new inspection. Passwords are intentionally excluded from the fragment and must be entered
again for protected shares.
## WebSocket protocol ## WebSocket protocol
All messages are JSON. Each WebSocket accepts exactly one complete job request and closes after its terminal response. A client that does not send a request within 10 seconds is disconnected. Passwords are accepted only in client messages and are never placed in URLs or echoed in status responses. All messages are JSON. Each WebSocket accepts exactly one complete job request and closes after its terminal response. A client that does not send a request within 10 seconds is disconnected. Passwords are accepted only in client messages and are never placed in URLs or echoed in status responses.
+40
View File
@@ -21,6 +21,43 @@ let jobCompleted = false;
let abortRequested = false; let abortRequested = false;
let syncRunning = false; let syncRunning = false;
function normalizedShareUrl(value) {
try {
const url = new URL(value.trim());
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password) return undefined;
const marker = url.pathname.lastIndexOf('/share/');
if (marker < 0) return undefined;
const credential = url.pathname.slice(marker + 7).replace(/\/+$/, '');
if (!credential || credential.includes('/')) return undefined;
url.search = '';
url.hash = '';
return url.toString();
} catch {
return undefined;
}
}
function updateShareFragment() {
const left = normalizedShareUrl($('#leftUrl').value);
const right = normalizedShareUrl($('#rightUrl').value);
if (!left || !right) {
if (location.hash) history.replaceState(null, '', `${location.pathname}${location.search}`);
return;
}
const fragment = new URLSearchParams({ left, right });
history.replaceState(null, '', `${location.pathname}${location.search}#${fragment}`);
}
function restoreShareFragment() {
const fragment = new URLSearchParams(location.hash.slice(1));
const left = normalizedShareUrl(fragment.get('left') || '');
const right = normalizedShareUrl(fragment.get('right') || '');
if (!left || !right) return false;
$('#leftUrl').value = left;
$('#rightUrl').value = right;
return true;
}
function openJob(request) { function openJob(request) {
if (socket && socket.readyState !== WebSocket.CLOSED) { if (socket && socket.readyState !== WebSocket.CLOSED) {
ui.message.textContent = 'Please wait for the current job to finish.'; ui.message.textContent = 'Please wait for the current job to finish.';
@@ -34,6 +71,7 @@ function openJob(request) {
jobCompleted = false; jobCompleted = false;
abortRequested = false; abortRequested = false;
ui.inspect.disabled = true; ui.inspect.disabled = true;
updateShareFragment();
setConnection('connecting', 'Connecting'); setConnection('connecting', 'Connecting');
jobSocket.addEventListener('open', () => { jobSocket.addEventListener('open', () => {
@@ -242,6 +280,7 @@ function handle(message) {
} }
ui.inspect.addEventListener('click', inspect); ui.inspect.addEventListener('click', inspect);
for (const input of [$('#leftUrl'), $('#rightUrl')]) input.addEventListener('input', updateShareFragment);
for (const input of $$('input')) input.addEventListener('keydown', (event) => { for (const input of $$('input')) input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') inspect(); if (event.key === 'Enter') inspect();
}); });
@@ -254,3 +293,4 @@ ui.abort.addEventListener('click', () => {
}); });
setConnection('idle', 'Idle'); setConnection('idle', 'Idle');
if (restoreShareFragment()) inspect();