<!-- SOLUV · part 476 -->
 'empty', S.msgs.length ? liveText() : 'No messages yet'); };
      es.onerror = () => { if (S.disposed) return; setStatus('reconnecting', 'Live dropped: reconnecting\u2026 (' + liveText() + ' shown)'); if (es.readyState === 2) { S.es = null; S.retry = setTimeout(openStream, 3000); } };
    };
    await get();
  }
  /* The site lobby: GET /api/chat/lobby/messages?after=N&limit=100 (public read, paged oldest first) + SSE /api/chat/lobby/events.
     Read-only use of the lobby's own endpoints; nothing in the lobby code changes. Its stream has no replay, so every
     (re)connect backfills with ?after=lastId. Duplicates are impossible: every message is keyed by its lobby id. */
  async function startLobby(base) {
    const fail = e => { S.errors.push(e); console.error('[soluv-wall] ' + e); setStatus('error', e + ' (retrying in 5 s)'); S.retry = setTimeout(backfill, 5000); };
    let first = true;
    const backfill = async () => {
      if (first) setStatus('loading', 'Opening the lobby');
      for (let pages = 0; pages < 1000; pages++) {
        let r; try { r = await fetch(base + '/messages?after=' + S.cursor + '&limit=100', { cache: 'no-store', credentials: 'same-origin' }); }
        catch (e) { return fail('Lobby unreachable: ' + (e.message || e)); }
        if (r.status === 401 || r.status === 403 || r.status === 404) { setStatus('login', 'The lobby is not public here: log in on epistles.xyz to read it (HTTP ' + r.status + ')'); return; }
        if (!r.ok) return fail('Lobby offline: HTTP ' + r.status);
        const txt = await r.text(); S.bytes += txt.length;
        let j; try { j = JSON.parse(txt); } catch (e) { return fail('Lobby sent bad JSON: ' + e.message); }
        const got = j.messages || []; addMsgs(got); updateMsgs(j.updated, j.deleted);
        if (got.length < 100) break;
      }
      first = false;
      setStatus(S.msgs.length ? 'live' : 'empty', S.msgs.length ? liveText() : 'No messages yet');
      openLobbyStream();
    };
    // The lobby's own SSE stream goes through the site's gzip compression, which holds events back (checked live:
    // not even ': connected' arrives), so the wall follows the lobby the safe way: ?after=lastId every 3 s while the
    // page is visible (a few hundred bytes when nothing is new). No lobby code changes needed.
    const openLobbyStream = () => {
      if (S.poll || S.disposed) return;
      const tick = () => { if (S.disposed || (typeof document !== 'undefined' && document.hidden)) return; backfillQuiet().catch(e => setStatus('error', 'Lobby check failed: ' + (e.message || e) + ' (retrying)')); };
      S.poll = setInterval(tick, 3000);
      if (S.msgs.length) setStatus('live', liveText());
    };
    const backfillQuiet = async () => { const r = await fetch(base + '/messages?after=' + S.cursor + '&limit=100', { cache: 'no-store', credentials: 'same-origin' }); if (!r.ok) { setStatus('error', 'Lobby offline: HTTP ' + r.status + ' (retrying)'); return; } const t = await r.text(); S.pollBytes = (S.pollBytes || 0) + t.length; const j = JSON.parse(t); const a = addMsgs(j.messages || []), u = updateMsgs(j.updated, j.deleted); if (a || u || S.status.state !== 'live') setStatus(S.msgs.length ? 'live' : 'empty', S.msgs.length ? liveText() : 'No messages yet'); };
    await backfill();
  }

  /* Posting from the 3D world goes into the SAME room through its own POST endpoint, with the viewer's own
     logged-in session and CSRF token (read from Playa's existing /api/soluv-room/me). No new auth, no impersonation.
     The posted message comes back with its id, so the stream's copy of it is dropped as a duplicate. */
  async function whoAmI() {
    const r = await fetch(origin + '/api/soluv-room/me', { cache: 'no-store', credentials: 'same-origin' });
    if (!r.ok) throw new Error('could not check who you are (HTTP ' + r.status + ')');
    return r.json();
  }
  async function post(text) {
    text = String(text || '').trim();
    if (!text) return { ok: false, error: 'Write something first' };
    if (!source || source.type === 'static') return { ok: false, error: 'This wall has no room to post to' };
    const me = await whoAmI();
    if (!me.username) return { ok: false, login: true, error: 'Log in on epistles.xyz to post' };
    const lobby = source.type === 'lobby';
    const r = await fetch(lobby ? source.base + '/message' : source.base, { method: 'POST', credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': me.csrf || '' },
      body: JSON.stringify(lobby ? { body: text } : { msg: text, localKey: 'w' + Date.now().toString(36) }) });
    let j = null; try { j = await r.json(); } catch (e) { j = { error: 'HTTP ' + r.status }; }
    if (!r.ok || j.ok === false) {
      // honest limits, straight from the room's own server: lobby = 10 posts a minute per person (each person has their
      // own budget, so ten people side by side never block each other) and each lobby post spends 