diff --git a/server.js b/server.js index a7fdbee..794f79d 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,21 @@ const fs = require('fs'); const http = require('http'); const app = express(); -app.use(express.json()); + +// Capture raw body for webhook signature verification before JSON parsing +app.use((req, res, next) => { + if (req.path === '/webhook/gitea') { + let raw = ''; + req.on('data', chunk => { raw += chunk; }); + req.on('end', () => { + req.rawBody = raw; + try { req.body = JSON.parse(raw); } catch { req.body = {}; } + next(); + }); + } else { + express.json()(req, res, next); + } +}); const PORT = parseInt(process.env.PORT || '3060'); const BASE = (process.env.BASE_PATH || '/feedback-tool').replace(/\/$/, ''); @@ -41,7 +55,7 @@ function giteaRequest(method, path, body) { }); } -// GET /api/issues?repo=owner/repo — list open issues +// GET /api/issues?repo=owner/repo app.get(['/api/issues', BASE + '/api/issues'], async (req, res) => { const { repo } = req.query; if (!repo) return res.status(400).json({ error: 'repo required' }); @@ -93,6 +107,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { return { repo: s && s.getAttribute('data-repo') }; })(); + // ── Styles ──────────────────────────────────────────────────────────────── var style = document.createElement('style'); style.textContent = \` #_fb-fab { position:fixed; bottom:1.5rem; right:1.5rem; width:54px; height:54px; border-radius:50%; @@ -137,13 +152,43 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { .fb-comment-btn:hover { background:#333; opacity:1; } .fb-empty { color:#555; font-size:.85rem; text-align:center; padding:1.5rem 0; } .fb-loading { color:#555; font-size:.85rem; text-align:center; padding:1rem 0; } + /* Picker row inside new-issue form */ + .fb-picker-row { display:flex; align-items:center; gap:.6rem; } + .fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem; + padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; } + .fb-picker-btn:hover { background:#223322; } + .fb-picker-hint { color:#444; font-size:.75rem; } + .fb-snippet-preview { background:#0a0a0a; border:1px solid #2a2a2a; border-radius:6px; + padding:.5rem .7rem; font-size:.72rem; color:#7c9ef5; font-family:monospace; + white-space:pre-wrap; word-break:break-all; max-height:80px; overflow-y:auto; } + /* Picker overlay */ + #_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; } + #_fb-picker-shield.active { display:block; } + #_fb-picker-toast { position:fixed; top:1rem; left:50%; transform:translateX(-50%); + background:#1a1a1a; border:1px solid #7c9ef5; border-radius:8px; + padding:.6rem 1.1rem; color:#e0e0e0; font-size:.85rem; z-index:200000; + font-family:'Segoe UI',system-ui,sans-serif; display:none; box-shadow:0 4px 16px rgba(0,0,0,.5); } + #_fb-picker-toast.active { display:block; } + ._fb-hover-highlight { outline:2px solid #7c9ef5 !important; outline-offset:2px !important; } \`; document.head.appendChild(style); + // ── FAB ─────────────────────────────────────────────────────────────────── var fab = document.createElement('button'); fab.id = '_fb-fab'; fab.innerHTML = '💬'; fab.title = 'Feedback'; document.body.appendChild(fab); + // ── Picker shield (invisible overlay during pick mode) ──────────────────── + var shield = document.createElement('div'); + shield.id = '_fb-picker-shield'; + document.body.appendChild(shield); + + var toast = document.createElement('div'); + toast.id = '_fb-picker-toast'; + toast.textContent = '🎯 Click any element to capture it — Esc to cancel'; + document.body.appendChild(toast); + + // ── Main overlay ────────────────────────────────────────────────────────── var overlay = document.createElement('div'); overlay.id = '_fb-overlay'; overlay.innerHTML = \` @@ -161,6 +206,13 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
+
+ + Click to reference a page element +
+
@@ -173,6 +225,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { \`; document.body.appendChild(overlay); + // ── Open / close ────────────────────────────────────────────────────────── function openOverlay() { overlay.classList.add('open'); } function closeOverlay() { overlay.classList.remove('open'); } @@ -183,6 +236,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { overlay.addEventListener('click', function(e) { if (e.target === overlay) closeOverlay(); }); document.getElementById('_fb-hclose').addEventListener('click', closeOverlay); + // ── Tabs ────────────────────────────────────────────────────────────────── var currentTab = 'new'; document.querySelectorAll('.fb-tab').forEach(function(btn) { btn.addEventListener('click', function() { @@ -195,6 +249,99 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { }); }); + // ── Picker mode ─────────────────────────────────────────────────────────── + var pickerActive = false; + var pickerHighlighted = null; + var capturedSnippet = ''; + + function getSelector(el) { + if (!el || el === document.body) return 'body'; + var tag = el.tagName.toLowerCase(); + var id = el.id ? '#' + el.id : ''; + var cls = el.className && typeof el.className === 'string' + ? '.' + el.className.trim().split(/\\s+/).slice(0,2).join('.') : ''; + return tag + id + cls; + } + + function getSnippet(el) { + // Outer HTML truncated sensibly + var html = el.outerHTML || ''; + if (html.length > 500) { + // Just show tag + attrs + content preview + var inner = (el.textContent || '').trim().slice(0, 120); + var m = html.match(/^(<[^>]+>)/); + html = (m ? m[1] : '<' + el.tagName.toLowerCase() + '>') + (inner ? '\\n ' + inner + '…' : '') + '\\n'; + } + return html; + } + + function activatePicker() { + pickerActive = true; + overlay.classList.remove('open'); + shield.classList.add('active'); + toast.classList.add('active'); + } + + function deactivatePicker(snippet) { + pickerActive = false; + shield.classList.remove('active'); + toast.classList.remove('active'); + if (pickerHighlighted) { + pickerHighlighted.classList.remove('_fb-hover-highlight'); + pickerHighlighted = null; + } + overlay.classList.add('open'); + if (snippet) { + capturedSnippet = snippet; + document.getElementById('_fb-snippet').textContent = snippet; + document.getElementById('_fb-snippet-wrap').style.display = ''; + document.getElementById('_fb-pick-hint').textContent = '✓ Element captured — you can pick again'; + // Append to description + var desc = document.getElementById('_fb-desc'); + var tag = '\\n\\n**Picked element:**\\n\`\`\`html\\n' + snippet + '\\n\`\`\`'; + if (!desc.value.includes(tag)) desc.value += tag; + } + } + + document.getElementById('_fb-pick-btn').addEventListener('click', function() { + activatePicker(); + }); + + // Mousemove on page during picker: highlight hovered element + document.addEventListener('mousemove', function(e) { + if (!pickerActive) return; + var el = document.elementFromPoint(e.clientX, e.clientY); + // Skip our own UI elements + if (!el || el.closest('#_fb-picker-shield') || el.closest('#_fb-picker-toast')) return; + if (pickerHighlighted && pickerHighlighted !== el) { + pickerHighlighted.classList.remove('_fb-hover-highlight'); + } + pickerHighlighted = el; + el.classList.add('_fb-hover-highlight'); + }); + + // Click during picker: capture element + shield.addEventListener('click', function(e) { + if (!pickerActive) return; + e.preventDefault(); e.stopPropagation(); + // Find element underneath the shield + shield.style.display = 'none'; + var el = document.elementFromPoint(e.clientX, e.clientY); + shield.style.display = ''; + if (el && !el.closest('#_fb-picker-toast')) { + el.classList.remove('_fb-hover-highlight'); + deactivatePicker(getSnippet(el)); + } else { + deactivatePicker(null); + } + }); + + // Esc cancels picker + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && pickerActive) deactivatePicker(null); + }); + + // ── Submit ──────────────────────────────────────────────────────────────── document.getElementById('_fb-submit').addEventListener('click', function() { var title = document.getElementById('_fb-title').value.trim(); var body = document.getElementById('_fb-desc').value.trim(); @@ -211,6 +358,9 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { status.textContent='✓ Issue #' + d.issue_number + ' created!'; document.getElementById('_fb-title').value = ''; document.getElementById('_fb-desc').value = ''; + document.getElementById('_fb-snippet-wrap').style.display = 'none'; + document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element'; + capturedSnippet = ''; btn.disabled = false; setTimeout(function() { status.textContent=''; }, 3000); } else { @@ -219,6 +369,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { }).catch(function() { status.style.color='#f57c7c'; status.textContent='Network error.'; btn.disabled=false; }); }); + // ── Open issues tab ─────────────────────────────────────────────────────── function loadIssues() { var list = document.getElementById('_fb-issues-list'); list.innerHTML = '
Loading…
';