kilabor/admin.js

341 lines
14 KiB
JavaScript
Raw Normal View History

2026-04-07 21:03:33 +00:00
const http = require('http');
const fs = require('fs');
const path = require('path');
const DATA_FILE = path.join(__dirname, 'data/mac_nerds.json');
const CATS_FILE = path.join(__dirname, 'data/mac_nerds_cats.json');
const PORT = 3001;
const RISKS = ['safe', 'warning', 'danger'];
function readData() { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); }
function writeData(d) { fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 4), 'utf8'); }
function readCats() { return JSON.parse(fs.readFileSync(CATS_FILE, 'utf8')); }
function writeCats(c) { fs.writeFileSync(CATS_FILE, JSON.stringify(c, null, 2), 'utf8'); }
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function html(data, cats) {
const catOptions = cats.map(c => `<option value="${c}">${c}</option>`).join('');
const catBadges = cats.map(c => `
<div class="cat-badge">
<span>${esc(c)}</span>
<button class="cat-del" onclick="deleteCat('${esc(c)}')" title="Löschen">✕</button>
</div>`).join('');
const rows = data.map((cmd, i) => `
<tr>
<td><span class="cat">${esc(cmd.cat)}</span></td>
<td>${esc(cmd.title)}</td>
<td><code>${esc(cmd.cmd)}</code></td>
<td class="desc-cell">${esc(cmd.desc)}</td>
<td><span class="risk risk-${cmd.risk}">${cmd.risk}</span></td>
<td class="actions">
<button onclick="editRow(${i})">✏</button>
<button class="del" onclick="delRow(${i})">✕</button>
</td>
</tr>`).join('');
const json = JSON.stringify(data).replace(/</g,'\\u003c');
const catsJson = JSON.stringify(cats).replace(/</g,'\\u003c');
return `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MAC NERDS // ADMIN</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0a0a0a; color: #d4af37; font-family: 'Courier New', monospace; padding: 30px 20px; }
h1 { color: #ff4500; letter-spacing: 8px; font-size: 1.6rem; margin-bottom: 6px; }
p.sub { color: rgba(212,175,55,0.4); font-size: 0.7rem; letter-spacing: 3px; margin-bottom: 30px; }
.box { background: #111; border: 1px solid rgba(184,115,51,0.3); padding: 24px; margin-bottom: 24px; }
.box h2 { font-size: 0.85rem; letter-spacing: 4px; color: #b87333; margin-bottom: 18px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-grid .full { grid-column: 1 / -1; }
label { display: block; font-size: 0.65rem; letter-spacing: 2px; color: rgba(212,175,55,0.5); margin-bottom: 4px; }
input, textarea, select {
width: 100%; background: #000; border: 1px solid rgba(184,115,51,0.25);
color: #d4af37; font-family: 'Courier New', monospace; font-size: 0.8rem;
padding: 8px 10px; outline: none; transition: border-color 0.2s;
}
input:focus, textarea:focus, select:focus { border-color: #d4af37; }
textarea { resize: vertical; min-height: 60px; }
select option { background: #111; }
.btn-row { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
button {
background: transparent; border: 1px solid rgba(212,175,55,0.4);
color: #d4af37; font-family: 'Courier New', monospace; font-size: 0.75rem;
letter-spacing: 2px; padding: 8px 20px; cursor: pointer; transition: all 0.2s;
}
button:hover { background: rgba(212,175,55,0.1); border-color: #d4af37; }
button.primary { border-color: #ff4500; color: #ff4500; }
button.primary:hover { background: rgba(255,69,0,0.1); }
button.del { border-color: rgba(255,69,0,0.3); color: rgba(255,69,0,0.6); padding: 4px 10px; }
button.del:hover { background: rgba(255,69,0,0.1); border-color: #ff4500; color: #ff4500; }
button.reset-btn { border-color: rgba(212,175,55,0.2); color: rgba(212,175,55,0.4); }
button.green { border-color: rgba(76,175,80,0.5); color: #4caf50; }
button.green:hover { background: rgba(76,175,80,0.1); }
/* KATEGORIEN */
.cats-wrap { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.cat-badge {
display: flex; align-items: center; gap: 6px;
background: rgba(184,115,51,0.1); border: 1px solid rgba(184,115,51,0.3);
padding: 4px 10px; font-size: 0.75rem; color: #b87333;
}
.cat-del {
background: transparent; border: none; color: rgba(255,69,0,0.5);
cursor: pointer; font-size: 0.7rem; padding: 0 2px; letter-spacing: 0;
}
.cat-del:hover { color: #ff4500; background: transparent; }
.cat-add-row { display: flex; gap: 10px; align-items: flex-end; margin-top: 16px; }
.cat-add-row input { max-width: 200px; }
/* TABELLE */
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.count { font-size: 0.65rem; color: rgba(212,175,55,0.4); letter-spacing: 2px; }
table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
th { text-align: left; color: rgba(212,175,55,0.4); font-size: 0.6rem; letter-spacing: 3px;
border-bottom: 1px solid rgba(184,115,51,0.2); padding: 6px 10px; }
td { padding: 10px; border-bottom: 1px solid rgba(184,115,51,0.1); vertical-align: top; }
td.desc-cell { font-size: 0.7rem; color: rgba(212,175,55,0.5); max-width: 220px; }
tr:hover td { background: rgba(212,175,55,0.03); }
code { color: #7ec8a4; font-size: 0.72rem; word-break: break-all; }
td.actions { white-space: nowrap; }
td.actions button { padding: 4px 10px; margin-right: 4px; }
.cat { font-size: 0.6rem; letter-spacing: 2px; padding: 2px 7px; border: 1px solid rgba(184,115,51,0.4); color: #b87333; }
.risk { font-size: 0.6rem; letter-spacing: 2px; padding: 2px 7px; border: 1px solid; white-space: nowrap; }
.risk-safe { color: #4caf50; border-color: rgba(76,175,80,0.4); }
.risk-warning { color: #ff9800; border-color: rgba(255,152,0,0.4); }
.risk-danger { color: #ff4500; border-color: rgba(255,69,0,0.4); }
.toast { position: fixed; bottom: 30px; right: 30px; background: #111;
border: 1px solid #4caf50; color: #4caf50; padding: 10px 20px;
font-size: 0.75rem; letter-spacing: 2px; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
.toast.err { border-color: #ff4500; color: #ff4500; }
.toast.show { opacity: 1; }
@media (max-width: 700px) {
.form-grid { grid-template-columns: 1fr; }
table { font-size: 0.68rem; }
td.desc-cell { display: none; }
}
</style>
</head>
<body>
<h1>MAC NERDS</h1>
<p class="sub">// LOKALES ADMIN-PANEL · localhost:${PORT}</p>
<!-- KATEGORIEN -->
<div class="box">
<h2>KATEGORIEN</h2>
<div class="cats-wrap" id="cats-wrap">${catBadges}</div>
<div class="cat-add-row">
<div>
<label>NEUE KATEGORIE</label>
<input type="text" id="new-cat" placeholder="z.B. sicherheit" maxlength="30"
onkeydown="if(event.key==='Enter') addCat()">
</div>
<button class="green" onclick="addCat()">[ + HINZUFÜGEN ]</button>
</div>
</div>
<!-- BEFEHL FORMULAR -->
<div class="box">
<h2 id="form-title">+ NEUER BEFEHL</h2>
<form id="cmd-form">
<input type="hidden" id="edit-index" value="-1">
<div class="form-grid">
<div>
<label>KATEGORIE</label>
<select id="f-cat">${catOptions}</select>
</div>
<div>
<label>RISIKO</label>
<select id="f-risk">${RISKS.map(r=>`<option value="${r}">${r}</option>`).join('')}</select>
</div>
<div class="full">
<label>TITEL</label>
<input type="text" id="f-title" placeholder="z.B. Netzwerkqualität testen" required>
</div>
<div class="full">
<label>BEFEHL</label>
<input type="text" id="f-cmd" placeholder="z.B. networkQuality" required>
</div>
<div class="full">
<label>BESCHREIBUNG</label>
<textarea id="f-desc" placeholder="Was macht der Befehl?"></textarea>
</div>
</div>
<div class="btn-row">
<button type="submit" class="primary" id="submit-btn">[ SPEICHERN ]</button>
<button type="button" class="reset-btn" onclick="resetForm()">[ ABBRECHEN ]</button>
</div>
</form>
</div>
<!-- TABELLE -->
<div class="box">
<div class="table-header">
<h2>BEFEHLE</h2>
<span class="count" id="count-label">${data.length} EINTRÄGE</span>
</div>
<table>
<thead>
<tr><th>CAT</th><th>TITEL</th><th>BEFEHL</th><th>BESCHREIBUNG</th><th>RISIKO</th><th></th></tr>
</thead>
<tbody id="cmd-table">${rows}</tbody>
</table>
</div>
<div class="toast" id="toast"></div>
<script>
let DATA = ${json};
let CATS = ${catsJson};
function toast(msg, err) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast' + (err ? ' err' : '') + ' show';
setTimeout(() => t.classList.remove('show'), 2000);
}
// ── KATEGORIEN ──────────────────────────────
function renderCats() {
document.getElementById('cats-wrap').innerHTML = CATS.map(c => \`
<div class="cat-badge">
<span>\${c}</span>
<button class="cat-del" onclick="deleteCat('\${c}')" title="Löschen">✕</button>
</div>\`).join('');
const sel = document.getElementById('f-cat');
const cur = sel.value;
sel.innerHTML = CATS.map(c => \`<option value="\${c}">\${c}</option>\`).join('');
if (CATS.includes(cur)) sel.value = cur;
}
function addCat() {
const inp = document.getElementById('new-cat');
const val = inp.value.trim().toLowerCase().replace(/[^a-z0-9äöü-]/g, '');
if (!val) return;
if (CATS.includes(val)) { toast('Kategorie existiert bereits', true); return; }
CATS.push(val);
CATS.sort();
saveCats(() => { inp.value = ''; renderCats(); toast('✓ KATEGORIE GESPEICHERT'); });
}
function deleteCat(cat) {
const inUse = DATA.some(d => d.cat === cat);
if (inUse && !confirm(\`Kategorie "\${cat}" wird noch von Befehlen verwendet. Trotzdem löschen?\`)) return;
CATS = CATS.filter(c => c !== cat);
saveCats(() => { renderCats(); toast('✓ GELÖSCHT'); });
}
function saveCats(cb) {
fetch('/save-cats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(CATS)
}).then(r => r.json()).then(r => { if (r.ok) cb(); else toast('Fehler: ' + r.error, true); });
}
// ── BEFEHLE ──────────────────────────────────
function editRow(i) {
const c = DATA[i];
document.getElementById('edit-index').value = i;
document.getElementById('f-cat').value = c.cat;
document.getElementById('f-risk').value = c.risk;
document.getElementById('f-title').value = c.title;
document.getElementById('f-cmd').value = c.cmd;
document.getElementById('f-desc').value = c.desc;
document.getElementById('form-title').textContent = '✏ BEFEHL BEARBEITEN';
document.getElementById('submit-btn').textContent = '[ AKTUALISIEREN ]';
document.querySelector('.box:nth-child(2)').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('cmd-form').reset();
document.getElementById('edit-index').value = -1;
document.getElementById('form-title').textContent = '+ NEUER BEFEHL';
document.getElementById('submit-btn').textContent = '[ SPEICHERN ]';
}
function delRow(i) {
if (!confirm('Befehl löschen?')) return;
DATA.splice(i, 1);
saveData();
}
document.getElementById('cmd-form').addEventListener('submit', e => {
e.preventDefault();
const entry = {
cat: document.getElementById('f-cat').value,
title: document.getElementById('f-title').value.trim(),
cmd: document.getElementById('f-cmd').value.trim(),
desc: document.getElementById('f-desc').value.trim(),
risk: document.getElementById('f-risk').value,
};
const idx = parseInt(document.getElementById('edit-index').value);
if (idx >= 0) DATA[idx] = entry; else DATA.push(entry);
saveData();
resetForm();
});
function saveData() {
fetch('/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(DATA)
}).then(r => r.json()).then(r => {
if (r.ok) { toast('✓ GESPEICHERT'); setTimeout(() => location.reload(), 500); }
else toast('Fehler: ' + r.error, true);
});
}
</script>
</body>
</html>`;
}
const server = http.createServer((req, res) => {
const json = (status, obj) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj));
};
const body = (cb) => {
let b = '';
req.on('data', c => b += c);
req.on('end', () => cb(b));
};
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html(readData(), readCats()));
} else if (req.method === 'POST' && req.url === '/save') {
body(b => {
try { writeData(JSON.parse(b)); json(200, { ok: true }); }
catch(e) { json(400, { ok: false, error: e.message }); }
});
} else if (req.method === 'POST' && req.url === '/save-cats') {
body(b => {
try { writeCats(JSON.parse(b)); json(200, { ok: true }); }
catch(e) { json(400, { ok: false, error: e.message }); }
});
} else {
res.writeHead(404); res.end('Not found');
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`\n MAC NERDS ADMIN → http://localhost:${PORT}\n`);
});