diff --git a/public/admin/preview.html b/public/admin/preview.html
index 11a1372..0640d0b 100644
--- a/public/admin/preview.html
+++ b/public/admin/preview.html
@@ -34,6 +34,22 @@ document.head.insertAdjacentHTML('beforeend', ``);
await requireAuth();
+// Surface client-side errors on screen — a silent throw here used to leave the
+// panel stuck on "Waiting for server…" with no clue why.
+function showError(msg) {
+ let el = document.getElementById('perr');
+ if (!el) {
+ el = document.createElement('div');
+ el.id = 'perr';
+ el.style.cssText = 'position:fixed;bottom:0;left:0;right:0;max-height:35vh;overflow:auto;' +
+ 'background:rgba(120,0,0,.9);color:#fff;font:12px monospace;padding:8px;z-index:9999;white-space:pre-wrap';
+ document.body.appendChild(el);
+ }
+ el.textContent += msg + '\n';
+}
+window.addEventListener('error', e => showError('[error] ' + e.message + (e.filename ? ` (${e.filename}:${e.lineno})` : '')));
+window.addEventListener('unhandledrejection', e => showError('[rejection] ' + (e.reason?.message || e.reason)));
+
const COLORS = { Red: '#ff2678', Yellow: '#fff35d', Blue: '#51eaf1' };
const app = document.getElementById('app');
app.innerHTML = `${nav('preview')}
@@ -135,7 +151,13 @@ const clockSamples = []; let clockOffset = 0;
async function addGhost(rec) {
if (active.has(rec.uid)) return;
- const group = await buildGhost(rec, gradients, modelManifest, characters);
+ let group;
+ try {
+ group = await buildGhost(rec, gradients, modelManifest, characters);
+ } catch (e) {
+ showError(`ghost "${rec.name}" failed to build: ${e.message}`);
+ return;
+ }
group.userData.setHeight(ghostHeight);
const label = textSprite(rec.name, COLORS[rec.color] || '#fff');
label.scale.set(20, 5, 1);