From 0a6255731cb79d76fb563d2c50929ac4f9076eb6 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Fri, 13 Mar 2026 10:27:48 +0000 Subject: [PATCH] Initial commit: feedback-tool server + deploy workflow --- .gitea/workflows/deploy.yml | 14 ++++ .gitignore | 1 + package.json | 15 ++++ server.js | 159 ++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 package.json create mode 100644 server.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..7aef480 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,14 @@ +name: Deploy to dev.bl.pixeldev.eu +on: + push: + branches: [main] +jobs: + deploy: + runs-on: self-hosted + steps: + - uses: actions/checkout@v3 + - name: Sync and restart + run: | + rsync -av --delete --exclude='.git' --exclude='node_modules' $GITHUB_WORKSPACE/ /opt/feedback-tool/ + cd /opt/feedback-tool && npm install --production + pm2 restart feedback-tool diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..14dee2c --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "feedback-tool", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^5.2.1" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..0e67166 --- /dev/null +++ b/server.js @@ -0,0 +1,159 @@ +const express = require('express'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +app.use(express.json()); + +const PORT = parseInt(process.env.PORT || '3060'); +const BASE = (process.env.BASE_PATH || '/feedback-tool').replace(/\/$/, ''); +const FT_TOKEN = process.env.FT_TOKEN || fs.readFileSync('/root/feedback-tool.env','utf8').match(/FT_TOKEN=(.*)/)?.[1]?.trim(); +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'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + if (req.method === 'OPTIONS') return res.sendStatus(200); + next(); +}); + +// Serve widget.js — public, no auth +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'; + + 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' + }; + })(); + + 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; } + \`; + document.head.appendChild(style); + + var fab = document.createElement('button'); + 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 = \` + +

💬 Submit Feedback

+ + + +
+ \`; + document.body.appendChild(panel); + + fab.addEventListener('click', function() { panel.classList.toggle('open'); }); + document.getElementById('_fb-close').addEventListener('click', function() { panel.classList.remove('open'); }); + + document.getElementById('_fb-submit').addEventListener('click', function() { + var title = document.getElementById('_fb-title').value.trim(); + var body = document.getElementById('_fb-body').value.trim(); + var status = document.getElementById('_fb-status'); + var btn = document.getElementById('_fb-submit'); + if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; } + btn.disabled = true; status.style.color='#888'; status.textContent='Submitting...'; + fetch(FEEDBACK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: title, body: body, repo: cfg.repo, source_url: window.location.href }) + }).then(function(r) { return r.json(); }).then(function(d) { + if (d.issue_url) { + status.style.color='#6fcf6f'; + status.textContent='✓ Thanks! Issue #' + d.issue_number + ' created.'; + document.getElementById('_fb-title').value = ''; + document.getElementById('_fb-body').value = ''; + setTimeout(function() { panel.classList.remove('open'); status.textContent=''; btn.disabled=false; }, 2500); + } else { + status.style.color='#f57c7c'; status.textContent='Error: ' + (d.error||'unknown'); btn.disabled=false; + } + }).catch(function(e) { + status.style.color='#f57c7c'; status.textContent='Network error.'; btn.disabled=false; + }); + }); +})(); +`); +}); + +// POST /api/feedback — create Gitea issue +app.post(['/api/feedback', BASE + '/api/feedback'], (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'); + + const postData = JSON.stringify({ title, body: issueBody }); + const options = { + hostname: GITEA_HOST, port: GITEA_PORT, + path: `/api/v1/repos/${repo}/issues`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${FT_TOKEN}`, + 'Content-Length': Buffer.byteLength(postData) + } + }; + + const request = require('http').request(options, r => { + let data = ''; + r.on('data', c => data += c); + r.on('end', () => { + try { + const issue = JSON.parse(data); + if (issue.number) res.json({ ok: true, issue_number: issue.number, issue_url: issue.html_url }); + else res.status(500).json({ error: 'Failed to create issue', raw: data.slice(0,200) }); + } catch { res.status(500).json({ error: 'Parse error' }); } + }); + }); + request.on('error', e => res.status(500).json({ error: e.message })); + request.write(postData); + request.end(); +}); + +// Redirect base to registry +app.get([BASE, BASE + '/'], (req, res) => res.redirect('https://dev.bl.pixeldev.eu')); + +app.listen(PORT, '127.0.0.1', () => console.log(`feedback-tool on :${PORT} at ${BASE}`));