diff --git a/server.js b/server.js index 0e67166..a7fdbee 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,6 @@ const express = require('express'); -const https = require('https'); const fs = require('fs'); -const path = require('path'); +const http = require('http'); const app = express(); app.use(express.json()); @@ -12,7 +11,6 @@ const FT_TOKEN = process.env.FT_TOKEN || fs.readFileSync('/root/feedback-tool.en const GITEA_HOST = '127.0.0.1'; const GITEA_PORT = 3040; -// CORS for widget app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Content-Type'); @@ -21,46 +19,124 @@ app.use((req, res, next) => { next(); }); -// Serve widget.js — public, no auth +function giteaRequest(method, path, body) { + return new Promise((resolve, reject) => { + const postData = body ? JSON.stringify(body) : null; + const options = { + hostname: GITEA_HOST, port: GITEA_PORT, path, method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${FT_TOKEN}`, + ...(postData ? { 'Content-Length': Buffer.byteLength(postData) } : {}) + } + }; + const req = http.request(options, r => { + let data = ''; + r.on('data', c => data += c); + r.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(data); } }); + }); + req.on('error', reject); + if (postData) req.write(postData); + req.end(); + }); +} + +// GET /api/issues?repo=owner/repo — list open issues +app.get(['/api/issues', BASE + '/api/issues'], async (req, res) => { + const { repo } = req.query; + if (!repo) return res.status(400).json({ error: 'repo required' }); + try { + const issues = await giteaRequest('GET', `/api/v1/repos/${repo}/issues?state=open&type=issues&limit=20`); + res.json(Array.isArray(issues) ? issues.map(i => ({ + number: i.number, title: i.title, + body: (i.body || '').slice(0, 300), + created_at: i.created_at, + comments: i.comments, + url: i.html_url + })) : []); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// POST /api/feedback — create new issue +app.post(['/api/feedback', BASE + '/api/feedback'], async (req, res) => { + const { title, body, repo, source_url } = req.body; + if (!title) return res.status(400).json({ error: 'title required' }); + if (!repo) return res.status(400).json({ error: 'repo required' }); + const issueBody = [body || '', '', '---', `*Submitted via feedback widget from: ${source_url || 'unknown'}*`].join('\n'); + try { + const issue = await giteaRequest('POST', `/api/v1/repos/${repo}/issues`, { title, body: issueBody }); + if (issue.number) res.json({ ok: true, issue_number: issue.number, issue_url: issue.html_url }); + else res.status(500).json({ error: 'Failed', raw: JSON.stringify(issue).slice(0, 200) }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// POST /api/comment — add comment to existing issue +app.post(['/api/comment', BASE + '/api/comment'], async (req, res) => { + const { repo, issue_number, body } = req.body; + if (!repo || !issue_number || !body) return res.status(400).json({ error: 'repo, issue_number, body required' }); + try { + const comment = await giteaRequest('POST', `/api/v1/repos/${repo}/issues/${issue_number}/comments`, { body }); + if (comment.id) res.json({ ok: true, comment_id: comment.id }); + else res.status(500).json({ error: 'Failed', raw: JSON.stringify(comment).slice(0, 200) }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// Serve widget.js app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { const SELF = `https://dev.bl.pixeldev.eu${BASE}`; res.setHeader('Content-Type', 'application/javascript'); res.setHeader('Cache-Control', 'no-cache'); - res.send(` -(function() { - var FEEDBACK_URL = '${SELF}/api/feedback'; - + res.send(`(function() { + var SELF = '${SELF}'; var cfg = (function() { var s = document.currentScript || document.querySelector('script[data-repo]'); - return { - repo: s && s.getAttribute('data-repo'), - label: s && s.getAttribute('data-label') || 'Feedback' - }; + return { repo: s && s.getAttribute('data-repo') }; })(); var style = document.createElement('style'); style.textContent = \` - #_fb-fab { position:fixed; bottom:1.5rem; right:1.5rem; width:52px; height:52px; border-radius:50%; - background:#7c9ef5; color:#fff; font-size:22px; border:none; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,.4); - z-index:99998; display:flex; align-items:center; justify-content:center; transition:transform .15s; } - #_fb-fab:hover { transform:scale(1.1); } - #_fb-panel { position:fixed; bottom:5.5rem; right:1.5rem; width:320px; background:#1a1a1a; - border:1px solid #333; border-radius:14px; padding:1.2rem; z-index:99999; box-shadow:0 8px 32px rgba(0,0,0,.5); - font-family:'Segoe UI',system-ui,sans-serif; display:none; flex-direction:column; gap:.75rem; } - #_fb-panel.open { display:flex; } - #_fb-panel h3 { color:#fff; font-size:.95rem; margin:0; } - #_fb-panel input, #_fb-panel textarea { background:#111; border:1px solid #333; border-radius:6px; - padding:.5rem .7rem; color:#e0e0e0; font-size:.85rem; width:100%; font-family:inherit; outline:none; resize:vertical; } - #_fb-panel input:focus, #_fb-panel textarea:focus { border-color:#7c9ef5; } - #_fb-panel textarea { min-height:80px; } - #_fb-submit { background:#7c9ef5; color:#fff; border:none; border-radius:6px; padding:.55rem 1rem; - font-size:.85rem; font-weight:600; cursor:pointer; width:100%; transition:opacity .15s; } - #_fb-submit:hover { opacity:.85; } - #_fb-submit:disabled { opacity:.5; cursor:default; } - #_fb-status { font-size:.78rem; text-align:center; min-height:1rem; } - #_fb-close { position:absolute; top:.75rem; right:.9rem; background:none; border:none; color:#555; - cursor:pointer; font-size:1rem; line-height:1; } - #_fb-close:hover { color:#aaa; } + #_fb-fab { position:fixed; bottom:1.5rem; right:1.5rem; width:54px; height:54px; border-radius:50%; + background:#7c9ef5; color:#fff; font-size:22px; border:none; cursor:pointer; + box-shadow:0 4px 20px rgba(124,158,245,.4); z-index:99998; + display:flex; align-items:center; justify-content:center; transition:transform .15s,box-shadow .15s; } + #_fb-fab:hover { transform:scale(1.08); box-shadow:0 6px 24px rgba(124,158,245,.6); } + #_fb-overlay { position:fixed; inset:0; background:rgba(0,0,0,.6); backdrop-filter:blur(4px); + z-index:99998; display:none; align-items:center; justify-content:center; } + #_fb-overlay.open { display:flex; } + #_fb-modal { background:#161616; border:1px solid #2a2a2a; border-radius:16px; width:min(560px,96vw); + max-height:80vh; display:flex; flex-direction:column; box-shadow:0 24px 64px rgba(0,0,0,.7); + font-family:'Segoe UI',system-ui,sans-serif; overflow:hidden; } + #_fb-header { display:flex; align-items:center; justify-content:space-between; padding:1.1rem 1.4rem; + border-bottom:1px solid #222; flex-shrink:0; } + #_fb-header h2 { color:#fff; font-size:1rem; margin:0; font-weight:600; } + #_fb-hclose { background:none; border:none; color:#555; cursor:pointer; font-size:1.2rem; line-height:1; padding:0; } + #_fb-hclose:hover { color:#aaa; } + #_fb-tabs { display:flex; border-bottom:1px solid #222; flex-shrink:0; } + .fb-tab { flex:1; padding:.7rem; background:none; border:none; color:#666; font-size:.85rem; + cursor:pointer; font-family:inherit; border-bottom:2px solid transparent; transition:color .15s; } + .fb-tab.active { color:#7c9ef5; border-bottom-color:#7c9ef5; } + .fb-tab:hover { color:#aaa; } + #_fb-body { padding:1.2rem 1.4rem; overflow-y:auto; display:flex; flex-direction:column; gap:.8rem; } + .fb-input, .fb-textarea { background:#0d0d0d; border:1px solid #2a2a2a; border-radius:8px; + padding:.6rem .8rem; color:#e0e0e0; font-size:.875rem; width:100%; font-family:inherit; + outline:none; resize:vertical; transition:border-color .15s; } + .fb-input:focus, .fb-textarea:focus { border-color:#7c9ef5; } + .fb-textarea { min-height:90px; } + .fb-btn { background:#7c9ef5; color:#fff; border:none; border-radius:8px; padding:.65rem 1.2rem; + font-size:.875rem; font-weight:600; cursor:pointer; transition:opacity .15s; width:100%; } + .fb-btn:hover { opacity:.85; } + .fb-btn:disabled { opacity:.4; cursor:default; } + .fb-status { font-size:.8rem; text-align:center; min-height:1.1rem; } + .fb-issue-item { background:#0d0d0d; border:1px solid #222; border-radius:10px; padding:.9rem 1rem; } + .fb-issue-title { color:#e0e0e0; font-size:.9rem; font-weight:600; margin:0 0 .3rem; } + .fb-issue-meta { color:#555; font-size:.75rem; margin-bottom:.6rem; } + .fb-issue-body { color:#888; font-size:.8rem; margin-bottom:.7rem; white-space:pre-wrap; word-break:break-word; } + .fb-comment-area { display:flex; flex-direction:column; gap:.5rem; } + .fb-comment-input { min-height:55px; } + .fb-comment-btn { background:#2a2a2a; color:#ccc; font-size:.8rem; padding:.45rem .8rem; width:auto; align-self:flex-end; border-radius:6px; } + .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; } \`; document.head.appendChild(style); @@ -68,92 +144,126 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { fab.id = '_fb-fab'; fab.innerHTML = '💬'; fab.title = 'Feedback'; document.body.appendChild(fab); - var panel = document.createElement('div'); - panel.id = '_fb-panel'; panel.style.position = 'fixed'; - panel.innerHTML = \` - -
#\${issue.number} \${esc(issue.title)}
+ + \${preview ? '' + esc(preview) + '
' : ''} +