Initial commit: feedback-tool server + deploy workflow
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 3s
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 3s
This commit is contained in:
commit
0a6255731c
14
.gitea/workflows/deploy.yml
Normal file
14
.gitea/workflows/deploy.yml
Normal file
@ -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
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
15
package.json
Normal file
15
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
159
server.js
Normal file
159
server.js
Normal file
@ -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 = \`
|
||||||
|
<button id="_fb-close">✕</button>
|
||||||
|
<h3>💬 Submit Feedback</h3>
|
||||||
|
<input id="_fb-title" type="text" placeholder="Title (required)" />
|
||||||
|
<textarea id="_fb-body" placeholder="Describe your feedback..."></textarea>
|
||||||
|
<button id="_fb-submit">Send</button>
|
||||||
|
<div id="_fb-status"></div>
|
||||||
|
\`;
|
||||||
|
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}`));
|
||||||
Loading…
Reference in New Issue
Block a user