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' + el.tagName.toLowerCase() + '>';
+ }
+ 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…
';