dm / commit
feat: add Report Issue button with auto-collected debug info
public/index.html | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 94e0a64..9c0eded 100644 --- a/public/index.html +++ b/public/index.html @@ -823,6 +823,63 @@ body { } .share-link-trigger:hover { border-color: var(--gold-dim); color: var(--gold); } +/* โโ REPORT MODAL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ + +#report-modal { + display: none; + position: fixed; + inset: 0; + z-index: 200; + background: rgba(0,0,0,0.75); + align-items: center; + justify-content: center; +} +#report-modal.open { display: flex; } + +.report-inner { + background: linear-gradient(135deg, #0e0e1a, #141420); + border: 1px solid rgba(107,79,187,0.4); + border-radius: 10px; + padding: 28px 32px; + width: 520px; + max-width: 95vw; + max-height: 88vh; + overflow-y: auto; + box-shadow: 0 0 60px rgba(107,79,187,0.15), 0 20px 60px rgba(0,0,0,0.8); +} + +.report-title { + font-family: 'Cinzel', serif; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-bright); + margin-bottom: 20px; + letter-spacing: 0.06em; +} + +.report-debug-box { + font-family: monospace; + font-size: 0.7rem; + line-height: 1.5; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 4px; + padding: 10px 12px; + color: var(--text-dim); + white-space: pre-wrap; + word-break: break-all; + max-height: 180px; + overflow-y: auto; + margin-bottom: 16px; +} + +.report-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; +} + /* access code row */ .access-code-label { font-family: 'Cinzel', serif; @@ -924,7 +981,10 @@ body { <div class="chat-panel"> <div class="chat-header"> <div class="chat-header-title" id="header-title">Solo Dungeon Master</div> - <button class="new-game-btn" id="new-game-btn">โฆ New Game</button> + <div style="display:flex;gap:8px"> + <button class="new-game-btn" id="report-btn">๐ Report</button> + <button class="new-game-btn" id="new-game-btn">โฆ New Game</button> + </div> </div> <div id="messages"></div> @@ -1051,6 +1111,32 @@ body { </div> </div> +<!-- Report Issue modal --> +<div id="report-modal"> + <div class="report-inner"> + <div class="report-title">๐ Report an Issue</div> + + <div class="field-group" style="margin-bottom:14px"> + <label>What went wrong? <span style="color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">(optional โ describe what happened)</span></label> + <textarea id="report-desc" rows="3" placeholder="e.g. The game froze after I chose option 2โฆ" autocomplete="off" + 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;resize:vertical;margin-top:6px"></textarea> + </div> + + <div style="font-family:'Cinzel',serif;font-size:0.65rem;letter-spacing:0.12em;color:var(--text-dim);text-transform:uppercase;margin-bottom:6px">Debug info (included automatically)</div> + <div class="report-debug-box" id="report-debug-box"></div> + + <div class="report-actions"> + <button class="share-btn share-btn-primary" id="report-email-btn">๐ง Email โ no account needed</button> + <button class="share-btn share-btn-secondary" id="report-gitlab-btn">๐ GitLab Issue</button> + <button class="share-btn share-btn-secondary" id="report-copy-btn">๐ Copy info</button> + <button class="share-btn share-btn-secondary" id="report-close-btn">Cancel</button> + </div> + <p style="font-size:0.72rem;color:var(--text-dim);margin-top:10px;font-style:italic"> + Email creates a support ticket with no account required. GitLab Issue requires a free GitLab account. + </p> + </div> +</div> + <!-- Level up toast --> <div id="levelup-toast"> <div class="toast-icon">โญ</div> @@ -1060,6 +1146,19 @@ body { </div> <script> +// โโ ERROR BUFFER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Capture recent console.error calls so they can be included in bug reports. +const _recentErrors = []; +{ + const _orig = console.error.bind(console); + console.error = (...args) => { + _orig(...args); + const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '); + _recentErrors.push({ t: new Date().toISOString(), msg }); + if (_recentErrors.length > 30) _recentErrors.shift(); + }; +} + // โโ CRYPTO โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // Two implementations: AES-GCM (requires secure context) and a pure-JS // XOR stream cipher fallback for plain HTTP. Blobs are prefixed with @@ -1312,6 +1411,37 @@ function updateCostDisplay() { document.getElementById('cost-total').textContent = fmt(sessionCost); } +// โโ BUG REPORT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function buildDebugInfo(userDesc) { + const lines = []; + if (userDesc) { lines.push(`User description:\n${userDesc}`, ''); } + lines.push('--- App ---'); + lines.push(`Time: ${new Date().toISOString()}`); + lines.push(`URL: ${location.origin}${location.pathname}`); + lines.push(`Secure context: ${window.isSecureContext}`); + + if (gameState) { + const c = gameState.character; + lines.push('', '--- Session ---'); + lines.push(`Player ID: p_${getOrCreatePlayerId()}`); + if (gameState.gameId) lines.push(`Game ID: g_${gameState.gameId}`); + lines.push(`Character: ${c.name} โ Lv ${c.level} ${c.race} ${c.class}`); + lines.push(`HP: ${c.hp}/${c.maxHp} XP: ${c.xp} Gold: ${c.gold}g`); + lines.push(`Turns: ${conversationHistory.length} Cost: $${sessionCost.toFixed(5)}`); + } + + lines.push('', '--- Browser ---'); + lines.push(navigator.userAgent); + + if (_recentErrors.length > 0) { + lines.push('', '--- Recent errors ---'); + _recentErrors.forEach(e => lines.push(`[${e.t}]\n${e.msg}`)); + } + + return lines.join('\n'); +} + // โโ SESSION PERSISTENCE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ function saveSession() { @@ -2038,6 +2168,56 @@ document.getElementById('new-game-btn').addEventListener('click', () => { checkBeginReady(); }); +// โโ REPORT MODAL HANDLERS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +const SERVICE_DESK_EMAIL = 'contact-project+pavelsafronov-dm-82624720-issue-@incoming.gitlab.com'; +const GITLAB_ISSUES_URL = 'https://gitlab.com/PavelSafronov/dm/-/issues/new'; + +function openReportModal() { + document.getElementById('report-desc').value = ''; + document.getElementById('report-debug-box').textContent = buildDebugInfo(''); + document.getElementById('report-modal').classList.add('open'); +} + +document.getElementById('report-btn').addEventListener('click', openReportModal); + +document.getElementById('report-desc').addEventListener('input', () => { + document.getElementById('report-debug-box').textContent = + buildDebugInfo(document.getElementById('report-desc').value.trim()); +}); + +document.getElementById('report-close-btn').addEventListener('click', () => { + document.getElementById('report-modal').classList.remove('open'); +}); +document.getElementById('report-modal').addEventListener('click', e => { + if (e.target === document.getElementById('report-modal')) + document.getElementById('report-modal').classList.remove('open'); +}); + +document.getElementById('report-email-btn').addEventListener('click', () => { + const desc = document.getElementById('report-desc').value.trim(); + const body = buildDebugInfo(desc); + const title = desc ? desc.split('\n')[0].slice(0, 80) : 'Bug report: Solo Dungeon Master'; + window.open(`mailto:${SERVICE_DESK_EMAIL}?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`); +}); + +document.getElementById('report-gitlab-btn').addEventListener('click', () => { + const desc = document.getElementById('report-desc').value.trim(); + const body = buildDebugInfo(desc); + const title = desc ? desc.split('\n')[0].slice(0, 80) : 'Bug report'; + const params = new URLSearchParams({ 'issue[title]': title, 'issue[description]': body }); + window.open(`${GITLAB_ISSUES_URL}?${params}`); +}); + +document.getElementById('report-copy-btn').addEventListener('click', () => { + const desc = document.getElementById('report-desc').value.trim(); + navigator.clipboard.writeText(buildDebugInfo(desc)).then(() => { + const btn = document.getElementById('report-copy-btn'); + btn.textContent = 'โ Copied!'; + setTimeout(() => { btn.textContent = '๐ Copy info'; }, 2000); + }); +}); + // โโ INIT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ (async () => {