|
@@ -28,9 +28,41 @@ function updateStages(current, status) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// The reconstruction runs entirely server-side (a background worker persists
|
|
|
|
|
+// each job to disk). The client is JUST A VIEW: it remembers the active job id
|
|
|
|
|
+// (URL + localStorage) so a reload / reconnect / re-open resumes the SAME job,
|
|
|
|
|
+// and its polling shrugs off transient network drops instead of freezing.
|
|
|
|
|
+const LAST_JOB_KEY = 'genrecon:lastJob';
|
|
|
|
|
+let currentJob = null;
|
|
|
|
|
+
|
|
|
|
|
+function rememberJob(id) {
|
|
|
|
|
+ currentJob = id;
|
|
|
|
|
+ try { localStorage.setItem(LAST_JOB_KEY, id); } catch { /* private mode */ }
|
|
|
|
|
+ const u = new URL(location.href); u.searchParams.set('job', id); history.replaceState(null, '', u);
|
|
|
|
|
+}
|
|
|
|
|
+function forgetJob() {
|
|
|
|
|
+ currentJob = null;
|
|
|
|
|
+ try { localStorage.removeItem(LAST_JOB_KEY); } catch { /* private mode */ }
|
|
|
|
|
+ const u = new URL(location.href); u.searchParams.delete('job'); history.replaceState(null, '', u);
|
|
|
|
|
+}
|
|
|
|
|
+function stopPolling() { if (poll) { clearInterval(poll); poll = null; } }
|
|
|
|
|
+function startPolling(id) {
|
|
|
|
|
+ rememberJob(id);
|
|
|
|
|
+ stopPolling();
|
|
|
|
|
+ buildStages(); show('view-progress');
|
|
|
|
|
+ poll = setInterval(() => refresh(id), 2000);
|
|
|
|
|
+ refresh(id);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function refresh(id) {
|
|
async function refresh(id) {
|
|
|
let st;
|
|
let st;
|
|
|
- try { st = await (await fetch(`/api/jobs/${id}`)).json(); } catch { return; }
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const r = await fetch(`/api/jobs/${id}`);
|
|
|
|
|
+ if (r.status === 404) { // job unknown/expired → back to upload
|
|
|
|
|
+ stopPolling(); forgetJob(); show('view-upload'); return;
|
|
|
|
|
+ }
|
|
|
|
|
+ st = await r.json();
|
|
|
|
|
+ } catch { return; } // transient blip (reconnect etc.) → retry next tick, never freeze
|
|
|
$('bar-fill').style.width = (st.progress || 0) + '%';
|
|
$('bar-fill').style.width = (st.progress || 0) + '%';
|
|
|
$('progress-msg').textContent = st.message || st.status || '';
|
|
$('progress-msg').textContent = st.message || st.status || '';
|
|
|
$('progress-msg').classList.toggle('error', st.status === 'error');
|
|
$('progress-msg').classList.toggle('error', st.status === 'error');
|
|
@@ -41,10 +73,10 @@ async function refresh(id) {
|
|
|
} catch { /* no log yet */ }
|
|
} catch { /* no log yet */ }
|
|
|
|
|
|
|
|
if (st.status === 'done') {
|
|
if (st.status === 'done') {
|
|
|
- clearInterval(poll); poll = null;
|
|
|
|
|
|
|
+ stopPolling();
|
|
|
openViewer(`/api/jobs/${id}/scene.glb`);
|
|
openViewer(`/api/jobs/${id}/scene.glb`);
|
|
|
} else if (st.status === 'error') {
|
|
} else if (st.status === 'error') {
|
|
|
- clearInterval(poll); poll = null;
|
|
|
|
|
|
|
+ stopPolling();
|
|
|
$('progress-msg').textContent = '⚠ ' + (st.error || 'błąd rekonstrukcji');
|
|
$('progress-msg').textContent = '⚠ ' + (st.error || 'błąd rekonstrukcji');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -59,8 +91,8 @@ async function upload(file) {
|
|
|
try { res = await (await fetch('/api/jobs', { method: 'POST', body: fd })).json(); }
|
|
try { res = await (await fetch('/api/jobs', { method: 'POST', body: fd })).json(); }
|
|
|
catch (e) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = 'Nie udało się wysłać: ' + e; return; }
|
|
catch (e) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = 'Nie udało się wysłać: ' + e; return; }
|
|
|
if (res.error) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = res.error; return; }
|
|
if (res.error) { $('progress-msg').classList.add('error'); $('progress-msg').textContent = res.error; return; }
|
|
|
- poll = setInterval(() => refresh(res.id), 1500);
|
|
|
|
|
- refresh(res.id);
|
|
|
|
|
|
|
+ // Upload done → hand off to the durable server-side job; the client is now just a view.
|
|
|
|
|
+ startPolling(res.id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function openViewer(url) {
|
|
function openViewer(url) {
|
|
@@ -73,7 +105,8 @@ function openViewer(url) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function backToUpload() {
|
|
function backToUpload() {
|
|
|
- if (poll) { clearInterval(poll); poll = null; }
|
|
|
|
|
|
|
+ stopPolling();
|
|
|
|
|
+ forgetJob();
|
|
|
stopViewer();
|
|
stopViewer();
|
|
|
show('view-upload');
|
|
show('view-upload');
|
|
|
}
|
|
}
|
|
@@ -112,15 +145,23 @@ function init() {
|
|
|
wireJoystick();
|
|
wireJoystick();
|
|
|
if ('serviceWorker' in navigator) navigator.serviceWorker.register('./sw.js').catch(() => {});
|
|
if ('serviceWorker' in navigator) navigator.serviceWorker.register('./sw.js').catch(() => {});
|
|
|
|
|
|
|
|
- // Deep-link: /?job=<id> resumes (or opens) a specific reconstruction, so a
|
|
|
|
|
- // finished scene has a shareable URL and progress survives a reload.
|
|
|
|
|
- const jobId = new URLSearchParams(location.search).get('job');
|
|
|
|
|
- if (jobId) {
|
|
|
|
|
- buildStages();
|
|
|
|
|
- show('view-progress');
|
|
|
|
|
- poll = setInterval(() => refresh(jobId), 1500);
|
|
|
|
|
- refresh(jobId);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Resume the active job on load — from the URL (?job=, shareable) or, failing
|
|
|
|
|
+ // that, from localStorage. So closing the tab, losing signal, or reopening the
|
|
|
|
|
+ // app hours later drops you right back onto the still-running / finished scan.
|
|
|
|
|
+ let resumeId = new URLSearchParams(location.search).get('job');
|
|
|
|
|
+ if (!resumeId) { try { resumeId = localStorage.getItem(LAST_JOB_KEY); } catch { /* private mode */ } }
|
|
|
|
|
+ if (resumeId) startPolling(resumeId);
|
|
|
|
|
+
|
|
|
|
|
+ // Mobile browsers freeze timers on a backgrounded tab; re-sync the moment the
|
|
|
|
|
+ // tab is visible / the network is back, instead of waiting for the next tick.
|
|
|
|
|
+ const resync = () => {
|
|
|
|
|
+ if (!currentJob || document.hidden) return;
|
|
|
|
|
+ if ($('view-viewer').classList.contains('active')) return; // don't reload the walkthrough mid-explore
|
|
|
|
|
+ refresh(currentJob);
|
|
|
|
|
+ };
|
|
|
|
|
+ document.addEventListener('visibilitychange', resync);
|
|
|
|
|
+ window.addEventListener('online', resync);
|
|
|
|
|
+ window.addEventListener('focus', resync);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
init();
|
|
init();
|