fuzzyworld

dm / commit

feat: separate API key entry into setup modal shown before game UI

fad1ccdc5b5b0c4c92e52ead2d2028b3edf7de99 · Pavel Safronov · 2026-05-27 18:20:58 -0700 · parents 83889109f219

feat: separate API key entry into setup modal shown before game UI

First-time visitors see a full-screen setup modal to enter their API
key; returning visitors with a saved key skip straight to the creation
or resume screen. Share link generation (encrypted #k= URL + passphrase)
is now inside the setup modal rather than the creation screen, so it's
only accessible when a key is manually typed. Also adds a git-ignored
screenshots/ folder for local verification captures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 .gitignore        |   1 +
 public/index.html | 456 +++++++++++++++++++++++++++++++-----------------------
 2 files changed, 264 insertions(+), 193 deletions(-)

diff --git a/.gitignore b/.gitignore
index d273ca6..079df7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ node_modules/
 *.log
 .DS_Store
 .playwright-mcp/
+screenshots/
diff --git a/public/index.html b/public/index.html
index 9c0eded..55e3c79 100644
--- a/public/index.html
+++ b/public/index.html
@@ -729,7 +729,7 @@ body {
   display: none;
   position: fixed;
   inset: 0;
-  z-index: 200;
+  z-index: 400;
   background: rgba(0,0,0,0.75);
   align-items: center;
   justify-content: center;
@@ -823,6 +823,50 @@ body {
 }
 .share-link-trigger:hover { border-color: var(--gold-dim); color: var(--gold); }
 
+/* ── SETUP MODAL ────────────────────────────────── */
+
+#setup-modal {
+  display: none;
+  position: fixed;
+  inset: 0;
+  z-index: 300;
+  background: rgba(0,0,0,0.92);
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+}
+#setup-modal.open { display: flex; }
+
+.setup-inner {
+  background: linear-gradient(135deg, #14140a, #1e1c0a);
+  border: 1px solid var(--gold-dim);
+  border-radius: 12px;
+  padding: 36px 40px;
+  width: 480px;
+  max-width: 100%;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 0 80px rgba(201,168,76,0.15), 0 20px 60px rgba(0,0,0,0.8);
+}
+
+.setup-logo {
+  font-family: 'Cinzel', serif;
+  font-size: 1.4rem;
+  font-weight: 700;
+  color: var(--gold-light);
+  text-align: center;
+  letter-spacing: 0.06em;
+  margin-bottom: 6px;
+}
+
+.setup-sub {
+  font-style: italic;
+  color: var(--text-dim);
+  font-size: 0.95rem;
+  text-align: center;
+  margin-bottom: 24px;
+}
+
 /* ── REPORT MODAL ───────────────────────────────── */
 
 #report-modal {
@@ -906,43 +950,10 @@ body {
 <body>
 
 <!-- ── CHARACTER CREATION ───────────────────── -->
-<div id="creation-screen">
+<div id="creation-screen" style="display:none">
   <div class="creation-title"><img src="dice.png" alt="" style="height:0.85em;vertical-align:middle;margin-right:0.2em;filter:invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(0.9)"> Solo Dungeon Master</div>
   <div class="creation-sub">Powered by Claude — your story begins here</div>
 
-  <div class="creation-section">
-    <!-- Shown when no encrypted key in URL (developer / own key) -->
-    <div id="own-key-section">
-      <div class="section-label">Anthropic API Key</div>
-      <div class="field-group">
-        <label>API Key</label>
-        <input type="password" id="api-key-input" placeholder="sk-ant-api03-…" autocomplete="off" spellcheck="false">
-      </div>
-      <div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;min-height:18px">
-        <span id="key-saved-badge" style="font-size:0.75rem;color:var(--hp-full);display:none">✓ Saved on this device</span>
-        <button id="forget-key-btn" style="font-size:0.72rem;color:var(--text-dim);background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;display:none" type="button">Forget saved key</button>
-      </div>
-      <p style="font-size:0.78rem;color:var(--text-dim);margin-top:6px;font-style:italic">
-        Sent directly to Anthropic — never forwarded elsewhere. Saved to this device so you only enter it once.
-        Get one free at <a href="https://console.anthropic.com/" target="_blank" rel="noopener" style="color:var(--gold-dim)">console.anthropic.com</a>
-        — pay-as-you-go, a typical session costs a few cents.
-      </p>
-      <button class="share-link-trigger" id="open-share-modal">🔗 Generate share link</button>
-    </div>
-
-    <!-- Shown when #k=... is present in the URL (shared link recipient) -->
-    <div id="access-code-section" style="display:none">
-      <div class="section-label">Access Code</div>
-      <div class="field-group">
-        <label class="access-code-label">Enter the code you received</label>
-        <input type="password" id="access-code-input" placeholder="your passphrase" autocomplete="off" spellcheck="false">
-      </div>
-      <p style="font-size:0.78rem;color:var(--text-dim);margin-top:8px;font-style:italic">
-        You received a special link to this app. Enter the access code to unlock it.
-      </p>
-    </div>
-  </div>
-
   <div class="creation-section">
     <div class="section-label">Choose Your Race</div>
     <div class="selection-grid" id="race-grid"></div>
@@ -969,6 +980,10 @@ body {
 
   <button class="begin-btn" id="begin-btn" disabled>Begin Adventure</button>
 
+  <p style="margin-top:12px;text-align:center">
+    <button id="change-key-btn" style="font-size:0.72rem;color:var(--text-dim);background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0" type="button">⚙ Change API key</button>
+  </p>
+
   <p class="attribution">
     <a href="https://www.flaticon.com/authors/icongeek26" target="_blank" rel="noopener">Dice icon by Icongeek26</a> from
     <a href="https://www.flaticon.com/" target="_blank" rel="noopener">Flaticon</a>
@@ -1076,41 +1091,6 @@ body {
   </div>
 </div>
 
-<!-- Share link modal -->
-<div id="share-modal">
-  <div class="share-inner">
-    <div class="share-title">🔗 Generate Share Link</div>
-
-    <p id="share-http-warning" style="display:none;font-size:0.78rem;color:var(--hp-low);background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.25);border-radius:4px;padding:8px 12px;margin-bottom:14px">
-      ⚠️ Running on HTTP — using a simplified cipher. For stronger encryption, generate links from your deployed HTTPS site.
-    </p>
-
-    <div class="field-group" style="margin-bottom:14px">
-      <label>Your API Key</label>
-      <input type="password" id="share-key-input" placeholder="sk-ant-api03-…" autocomplete="off" spellcheck="false" style="width:100%;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-bright);font-family:'Crimson Text',serif;font-size:1rem;padding:10px 14px;outline:none">
-    </div>
-
-    <div class="field-group" style="margin-bottom:16px">
-      <label>Access Code <span style="color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">(passphrase your users will enter)</span></label>
-      <input type="text" id="share-pass-input" placeholder="e.g. goblin-treasure-42" autocomplete="off" spellcheck="false" style="width:100%;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-bright);font-family:'Crimson Text',serif;font-size:1rem;padding:10px 14px;outline:none">
-    </div>
-
-    <div class="share-row">
-      <button class="share-btn share-btn-primary" id="share-generate-btn">Generate</button>
-      <button class="share-btn share-btn-secondary" id="share-close-btn">Cancel</button>
-    </div>
-
-    <div id="share-result" style="display:none;margin-top:16px">
-      <div style="font-family:'Cinzel',serif;font-size:0.65rem;letter-spacing:0.12em;color:var(--gold);text-transform:uppercase;margin-bottom:6px">Share this URL</div>
-      <div class="share-url-box" id="share-url-display"></div>
-      <div class="share-row" style="margin-bottom:8px">
-        <button class="share-btn share-btn-primary" id="share-copy-btn">Copy URL</button>
-      </div>
-      <p class="share-note">⚠️ Share the access code <em>separately</em> from this link — by text, verbally, etc. The link alone is not enough to access the app.</p>
-    </div>
-  </div>
-</div>
-
 <!-- Report Issue modal -->
 <div id="report-modal">
   <div class="report-inner">
@@ -1137,6 +1117,75 @@ body {
   </div>
 </div>
 
+<!-- Setup / key entry modal (shown before game on first visit) -->
+<div id="setup-modal">
+  <div class="setup-inner">
+
+    <!-- Own-key panel -->
+    <div id="setup-own-panel">
+      <div class="setup-logo"><img src="dice.png" alt="" style="height:1em;vertical-align:middle;margin-right:0.3em;filter:invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(0.9)"> Solo Dungeon Master</div>
+      <p class="setup-sub">Powered by Claude — your story begins here</p>
+
+      <div style="font-family:'Cinzel',serif;font-size:0.75rem;letter-spacing:0.12em;color:var(--gold);text-transform:uppercase;margin-bottom:8px">Anthropic API Key</div>
+      <div class="field-group">
+        <input type="password" id="setup-key-input" placeholder="sk-ant-api03-…" autocomplete="off" spellcheck="false">
+      </div>
+      <div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;min-height:18px">
+        <span id="setup-key-saved-badge" style="font-size:0.75rem;color:var(--hp-full);display:none">✓ Saved on this device</span>
+        <button id="setup-forget-btn" style="font-size:0.72rem;color:var(--text-dim);background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;display:none" type="button">Forget saved key</button>
+      </div>
+      <p style="font-size:0.78rem;color:var(--text-dim);margin-top:6px;font-style:italic">
+        Sent directly to Anthropic — never forwarded elsewhere. Saved to this device so you only enter it once.
+        Get one free at <a href="https://console.anthropic.com/" target="_blank" rel="noopener" style="color:var(--gold-dim)">console.anthropic.com</a>
+        — pay-as-you-go, a typical session costs a few cents.
+      </p>
+
+      <div style="margin-top:18px;border-top:1px solid var(--border);padding-top:16px">
+        <button id="setup-share-toggle" class="share-link-trigger" style="margin-top:0">🔗 Share this app (optional)</button>
+        <div id="setup-share-panel" style="display:none;margin-top:14px">
+          <p id="setup-share-http-warning" style="display:none;font-size:0.78rem;color:var(--hp-low);background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.25);border-radius:4px;padding:8px 12px;margin-bottom:12px">
+            ⚠️ Running on HTTP — using a simplified cipher. For stronger encryption, generate links from the deployed HTTPS site.
+          </p>
+          <div class="field-group" style="margin-bottom:12px">
+            <label>Access Code <span style="color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">(passphrase your users will enter)</span></label>
+            <input type="text" id="setup-pass-input" placeholder="e.g. goblin-treasure-42" autocomplete="off" spellcheck="false" style="width:100%;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-bright);font-family:'Crimson Text',serif;font-size:1rem;padding:10px 14px;outline:none">
+          </div>
+          <div class="share-row">
+            <button class="share-btn share-btn-primary" id="setup-share-generate-btn">Generate</button>
+          </div>
+          <div id="setup-share-result" style="display:none;margin-top:12px">
+            <div style="font-family:'Cinzel',serif;font-size:0.65rem;letter-spacing:0.12em;color:var(--gold);text-transform:uppercase;margin-bottom:6px">Share this URL</div>
+            <div class="share-url-box" id="setup-share-url-display"></div>
+            <div class="share-row" style="margin-bottom:6px">
+              <button class="share-btn share-btn-primary" id="setup-share-copy-btn">Copy URL</button>
+            </div>
+            <p class="share-note">⚠️ Share the access code <em>separately</em> — by text, verbally, etc. The link alone is not enough.</p>
+          </div>
+        </div>
+      </div>
+
+      <button class="begin-btn" id="setup-continue-btn" disabled style="margin-top:24px;width:100%">Continue →</button>
+    </div>
+
+    <!-- Shared-key panel -->
+    <div id="setup-shared-panel" style="display:none">
+      <div class="setup-logo"><img src="dice.png" alt="" style="height:1em;vertical-align:middle;margin-right:0.3em;filter:invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(0.9)"> Solo Dungeon Master</div>
+      <p class="setup-sub">You received a special link to this app.<br>Enter your access code to continue.</p>
+
+      <div style="font-family:'Cinzel',serif;font-size:0.75rem;letter-spacing:0.12em;color:var(--gold);text-transform:uppercase;margin:20px 0 8px">Access Code</div>
+      <div class="field-group">
+        <input type="password" id="setup-passphrase-input" placeholder="your passphrase" autocomplete="off" spellcheck="false">
+      </div>
+      <p style="font-size:0.78rem;color:var(--text-dim);margin-top:6px;font-style:italic">
+        Enter the access code you received alongside this link.
+      </p>
+
+      <button class="begin-btn" id="setup-unlock-btn" disabled style="margin-top:24px;width:100%">Unlock →</button>
+    </div>
+
+  </div>
+</div>
+
 <!-- Level up toast -->
 <div id="levelup-toast">
   <div class="toast-icon">⭐</div>
@@ -1379,6 +1428,7 @@ let selectedClass = null;
 let gameState = null;
 let conversationHistory = [];
 let isBusy = false;
+let currentApiKey = null;
 
 // ── IDs ───────────────────────────────────────────────
 // player_id persists across games; game_id is fresh each adventure.
@@ -1460,20 +1510,14 @@ function saveSession() {
 }
 
 async function tryResumeSession() {
-  if (getSharedBlob()) return false;
+  if (!currentApiKey) return false;
   const sessionRaw = localStorage.getItem('dm_session');
   if (!sessionRaw) return false;
-  const keyRaw = localStorage.getItem('dm_api_key');
-  if (!keyRaw) return false;
 
-  let session, apiKey;
-  try {
-    session = JSON.parse(sessionRaw);
-    apiKey = await decryptFromStorage(keyRaw);
-    if (!apiKey) return false;
-  } catch { return false; }
+  let session;
+  try { session = JSON.parse(sessionRaw); } catch { return false; }
 
-  gameState = { ...session.gameState, apiKey };
+  gameState = { ...session.gameState, apiKey: currentApiKey };
   conversationHistory = session.conversationHistory || [];
 
   document.getElementById('creation-screen').style.display = 'none';
@@ -1528,94 +1572,7 @@ async function buildCreationScreen() {
   });
 
   document.getElementById('begin-btn').addEventListener('click', () => beginAdventure());
-
-  const sharedBlob = getSharedBlob();
-
-  if (sharedBlob) {
-    // Shared-link mode: show access code input, hide own-key section
-    document.getElementById('own-key-section').style.display = 'none';
-    document.getElementById('access-code-section').style.display = '';
-  } else {
-    // Own-key mode: restore from localStorage (stored encrypted)
-    let keyFromStorage = false;
-    const savedKey = await decryptFromStorage(localStorage.getItem('dm_api_key'));
-    if (savedKey) {
-      document.getElementById('api-key-input').value = savedKey;
-      document.getElementById('key-saved-badge').style.display = '';
-      document.getElementById('forget-key-btn').style.display = '';
-      keyFromStorage = true;
-    }
-
-    // Share button is only useful when the key was typed manually, not auto-loaded
-    function syncShareBtn() {
-      const hasManualKey = !keyFromStorage && document.getElementById('api-key-input').value.trim().length > 0;
-      document.getElementById('open-share-modal').style.display = hasManualKey ? '' : 'none';
-    }
-    syncShareBtn();
-
-    document.getElementById('forget-key-btn').addEventListener('click', () => {
-      localStorage.removeItem('dm_api_key');
-      localStorage.removeItem('dm_session');
-      document.getElementById('api-key-input').value = '';
-      document.getElementById('key-saved-badge').style.display = 'none';
-      document.getElementById('forget-key-btn').style.display = 'none';
-      keyFromStorage = false;
-      syncShareBtn();
-    });
-
-    document.getElementById('api-key-input').addEventListener('input', () => {
-      document.getElementById('key-saved-badge').style.display = 'none';
-      keyFromStorage = false;
-      syncShareBtn();
-    });
-
-    // Share modal
-    document.getElementById('open-share-modal').addEventListener('click', () => {
-      const cur = document.getElementById('api-key-input').value.trim();
-      if (cur) document.getElementById('share-key-input').value = cur;
-      document.getElementById('share-result').style.display = 'none';
-      document.getElementById('share-http-warning').style.display =
-        window.isSecureContext ? 'none' : '';
-      document.getElementById('share-modal').classList.add('open');
-    });
-
-    document.getElementById('share-close-btn').addEventListener('click', () => {
-      document.getElementById('share-modal').classList.remove('open');
-    });
-
-    document.getElementById('share-modal').addEventListener('click', e => {
-      if (e.target === document.getElementById('share-modal'))
-        document.getElementById('share-modal').classList.remove('open');
-    });
-
-    document.getElementById('share-generate-btn').addEventListener('click', async () => {
-      const apiKey     = document.getElementById('share-key-input').value.trim();
-      const passphrase = document.getElementById('share-pass-input').value.trim();
-      if (!apiKey)     { alert('Enter the API key to encrypt.'); return; }
-      if (!passphrase) { alert('Enter an access code / passphrase.'); return; }
-      const btn = document.getElementById('share-generate-btn');
-      btn.disabled = true; btn.textContent = 'Encrypting…';
-      try {
-        const blob = await encryptApiKey(apiKey, passphrase);
-        const url  = `${location.origin}${location.pathname}#k=${blob}`;
-        document.getElementById('share-url-display').textContent = url;
-        document.getElementById('share-result').style.display = '';
-      } catch (err) {
-        alert(`Encryption failed: ${err.message}`);
-      } finally {
-        btn.disabled = false; btn.textContent = 'Generate';
-      }
-    });
-
-    document.getElementById('share-copy-btn').addEventListener('click', () => {
-      const url = document.getElementById('share-url-display').textContent;
-      navigator.clipboard.writeText(url).then(() => {
-        const btn = document.getElementById('share-copy-btn');
-        btn.textContent = '✓ Copied!';
-        setTimeout(() => btn.textContent = 'Copy URL', 2000);
-      });
-    });
-  }
+  document.getElementById('change-key-btn').addEventListener('click', () => openSetupModal(getSharedBlob() ? 'shared' : 'own'));
 }
 
 function checkBeginReady() {
@@ -1623,27 +1580,8 @@ function checkBeginReady() {
 }
 
 async function beginAdventure() {
-  let apiKey;
-  const sharedBlob = getSharedBlob();
-
-  if (sharedBlob) {
-    const passphrase = document.getElementById('access-code-input').value.trim();
-    if (!passphrase) { alert('Please enter your access code.'); return; }
-    const btn = document.getElementById('begin-btn');
-    btn.disabled = true; btn.textContent = 'Unlocking…';
-    try {
-      apiKey = await decryptApiKey(sharedBlob, passphrase);
-    } catch (err) {
-      alert(err.message.includes('HTTPS') ? err.message : 'Invalid access code. Please check and try again.');
-      btn.disabled = false; btn.textContent = 'Begin Adventure';
-      return;
-    }
-    btn.textContent = 'Begin Adventure';
-  } else {
-    apiKey = document.getElementById('api-key-input').value.trim();
-    if (!apiKey) { alert('Please enter your Anthropic API key.'); return; }
-    localStorage.setItem('dm_api_key', await encryptForStorage(apiKey));
-  }
+  if (!currentApiKey) { openSetupModal(getSharedBlob() ? 'shared' : 'own'); return; }
+  const apiKey = currentApiKey;
 
   const name = document.getElementById('char-name').value.trim() || `The ${selectedClass.name}`;
   const theme = document.getElementById('adventure-theme').value.trim();
@@ -2153,7 +2091,7 @@ document.getElementById('quick-actions').addEventListener('click', e => {
   if (btn && !isBusy) sendMessage(btn.dataset.action, true);
 });
 
-document.getElementById('new-game-btn').addEventListener('click', () => {
+document.getElementById('new-game-btn').addEventListener('click', async () => {
   if (!confirm('Start a new game? Your current adventure will be lost.')) return;
   document.getElementById('game-screen').classList.remove('visible');
   document.getElementById('creation-screen').style.display = '';
@@ -2165,9 +2103,129 @@ document.getElementById('new-game-btn').addEventListener('click', () => {
   isBusy = false;
   document.querySelectorAll('.sel-card').forEach(c => c.classList.remove('selected'));
   localStorage.removeItem('dm_session');
+  currentApiKey = (await decryptFromStorage(localStorage.getItem('dm_api_key'))) || null;
+  if (!currentApiKey) { openSetupModal('own'); return; }
   checkBeginReady();
 });
 
+// ── SETUP MODAL ───────────────────────────────────────
+
+function openSetupModal(mode) {
+  const isShared = mode === 'shared';
+  document.getElementById('setup-own-panel').style.display    = isShared ? 'none' : '';
+  document.getElementById('setup-shared-panel').style.display = isShared ? '' : 'none';
+  if (!isShared) {
+    // Pre-fill with currentApiKey if available (editing)
+    if (currentApiKey) {
+      document.getElementById('setup-key-input').value = currentApiKey;
+      document.getElementById('setup-key-saved-badge').style.display = localStorage.getItem('dm_api_key') ? '' : 'none';
+      document.getElementById('setup-forget-btn').style.display = localStorage.getItem('dm_api_key') ? '' : 'none';
+    }
+    syncSetupContinueBtn();
+  }
+  document.getElementById('setup-modal').classList.add('open');
+}
+
+function closeSetupModal() {
+  document.getElementById('setup-modal').classList.remove('open');
+}
+
+function syncSetupContinueBtn() {
+  const hasKey = document.getElementById('setup-key-input').value.trim().length > 0;
+  document.getElementById('setup-continue-btn').disabled = !hasKey;
+}
+
+// Own-key panel
+document.getElementById('setup-key-input').addEventListener('input', () => {
+  document.getElementById('setup-key-saved-badge').style.display = 'none';
+  syncSetupContinueBtn();
+});
+
+document.getElementById('setup-forget-btn').addEventListener('click', () => {
+  localStorage.removeItem('dm_api_key');
+  localStorage.removeItem('dm_session');
+  currentApiKey = null;
+  document.getElementById('setup-key-input').value = '';
+  document.getElementById('setup-key-saved-badge').style.display = 'none';
+  document.getElementById('setup-forget-btn').style.display = 'none';
+  syncSetupContinueBtn();
+});
+
+document.getElementById('setup-continue-btn').addEventListener('click', async () => {
+  const key = document.getElementById('setup-key-input').value.trim();
+  if (!key) return;
+  currentApiKey = key;
+  localStorage.setItem('dm_api_key', await encryptForStorage(key));
+  closeSetupModal();
+  if (!(await tryResumeSession())) {
+    document.getElementById('creation-screen').style.display = '';
+  }
+});
+
+// Share link inside setup modal
+document.getElementById('setup-share-toggle').addEventListener('click', () => {
+  const panel = document.getElementById('setup-share-panel');
+  const isOpen = panel.style.display !== 'none';
+  panel.style.display = isOpen ? 'none' : '';
+  if (!isOpen) {
+    document.getElementById('setup-share-http-warning').style.display =
+      window.isSecureContext ? 'none' : '';
+    document.getElementById('setup-share-result').style.display = 'none';
+  }
+});
+
+document.getElementById('setup-share-generate-btn').addEventListener('click', async () => {
+  const apiKey     = document.getElementById('setup-key-input').value.trim();
+  const passphrase = document.getElementById('setup-pass-input').value.trim();
+  if (!apiKey)     { alert('Enter your API key first.'); return; }
+  if (!passphrase) { alert('Enter an access code / passphrase.'); return; }
+  const btn = document.getElementById('setup-share-generate-btn');
+  btn.disabled = true; btn.textContent = 'Encrypting…';
+  try {
+    const blob = await encryptApiKey(apiKey, passphrase);
+    const url  = `${location.origin}${location.pathname}#k=${blob}`;
+    document.getElementById('setup-share-url-display').textContent = url;
+    document.getElementById('setup-share-result').style.display = '';
+  } catch (err) {
+    alert(`Encryption failed: ${err.message}`);
+  } finally {
+    btn.disabled = false; btn.textContent = 'Generate';
+  }
+});
+
+document.getElementById('setup-share-copy-btn').addEventListener('click', () => {
+  const url = document.getElementById('setup-share-url-display').textContent;
+  navigator.clipboard.writeText(url).then(() => {
+    const btn = document.getElementById('setup-share-copy-btn');
+    btn.textContent = '✓ Copied!';
+    setTimeout(() => btn.textContent = 'Copy URL', 2000);
+  });
+});
+
+// Shared-key (passphrase) panel
+document.getElementById('setup-passphrase-input').addEventListener('input', () => {
+  const hasPass = document.getElementById('setup-passphrase-input').value.trim().length > 0;
+  document.getElementById('setup-unlock-btn').disabled = !hasPass;
+});
+
+document.getElementById('setup-unlock-btn').addEventListener('click', async () => {
+  const passphrase = document.getElementById('setup-passphrase-input').value.trim();
+  if (!passphrase) return;
+  const btn = document.getElementById('setup-unlock-btn');
+  btn.disabled = true; btn.textContent = 'Unlocking…';
+  try {
+    currentApiKey = await decryptApiKey(getSharedBlob(), passphrase);
+  } catch (err) {
+    alert(err.message.includes('HTTPS') ? err.message : 'Invalid access code. Please check and try again.');
+    btn.disabled = false; btn.textContent = 'Unlock →';
+    return;
+  }
+  closeSetupModal();
+  if (!(await tryResumeSession())) {
+    document.getElementById('creation-screen').style.display = '';
+  }
+});
+
 // ── REPORT MODAL HANDLERS ─────────────────────────────
 
 const SERVICE_DESK_EMAIL = 'contact-project+pavelsafronov-dm-82624720-issue-@incoming.gitlab.com';
@@ -2221,10 +2279,22 @@ document.getElementById('report-copy-btn').addEventListener('click', () => {
 // ── INIT ──────────────────────────────────────────────
 
 (async () => {
-  // Always build the creation screen so grids/listeners are ready for "New Game"
   await buildCreationScreen();
-  // Attempt resume — overwrites the game/creation screen state if a session exists
-  await tryResumeSession();
+
+  // Load own key from storage if available
+  const keyRaw = localStorage.getItem('dm_api_key');
+  if (keyRaw) currentApiKey = await decryptFromStorage(keyRaw) || null;
+
+  if (currentApiKey) {
+    // Key known — resume session or show creation screen
+    if (!(await tryResumeSession())) {
+      document.getElementById('creation-screen').style.display = '';
+    }
+  } else if (getSharedBlob()) {
+    openSetupModal('shared');
+  } else {
+    openSetupModal('own');
+  }
 })().catch(console.error);
 </script>
 </body>