added stuf
This commit is contained in:
+65
-12
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="admin-shell">
|
||||
@@ -40,19 +41,43 @@
|
||||
</section>
|
||||
|
||||
<section id="admin-shell-content" class="admin-shell-content" hidden>
|
||||
<section class="panel admin-overview-panel">
|
||||
<div>
|
||||
<p class="hero-kicker">Control Center</p>
|
||||
<h2>Manage the live list and incoming runs</h2>
|
||||
<p class="muted">Use the first panel to edit placements and the second panel to review new run submissions.</p>
|
||||
<section class="admin-tabs">
|
||||
<button type="button" class="admin-tab active" data-tab="stats">Stats</button>
|
||||
<button type="button" class="admin-tab" data-tab="list">List Editor</button>
|
||||
<button type="button" class="admin-tab" data-tab="runs">Run Queue</button>
|
||||
<button type="button" class="admin-tab" data-tab="bugs">Bug Reports</button>
|
||||
<button type="button" class="admin-tab" data-tab="import">Import</button>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-stats">
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Statistics</p>
|
||||
<h2>Overview</h2>
|
||||
<p id="admin-stats-status" class="muted">Loading stats...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-overview-badges">
|
||||
<span>Live list editor</span>
|
||||
<span>Run moderation</span>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-box">
|
||||
<strong id="stat-total-runs">--</strong>
|
||||
<span>Total Runs</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-pending-runs">--</strong>
|
||||
<span>Pending</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-levels-count">--</strong>
|
||||
<span>Levels</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-bug-reports">--</strong>
|
||||
<span>Open Reports</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel">
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-list" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">List Editor</p>
|
||||
@@ -82,7 +107,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel">
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-runs" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Run Queue</p>
|
||||
@@ -111,12 +136,40 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel">
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-bugs" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Bug Reports</p>
|
||||
<h2>View and manage bug reports</h2>
|
||||
<p id="bug-reports-admin-status" class="muted">Loading bug reports...</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<input id="bug-report-search" type="text" placeholder="Search reports..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:100px">Category</th>
|
||||
<th style="width:100px">Status</th>
|
||||
<th style="width:160px">Submitted By</th>
|
||||
<th>Subject</th>
|
||||
<th style="width:180px">Submitted</th>
|
||||
<th style="width:180px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bug-reports-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-import" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Import tool</p>
|
||||
<h2>External lists & bulk approve</h2>
|
||||
<p id="import-status" class="muted">Pull records from Pointercrate or AREDL (AREDL needs a server API key or token). Targeted import loads victories from the API by player or level, adds them as approved runs with notes set to “Valid run”. Bulk approve approves pending FEDL submissions only. Full imports run one page at a time to limit rate usage.</p>
|
||||
<p id="import-status" class="muted">Pull records from Pointercrate or AREDL (AREDL needs a server API key or token). Targeted import loads victories from the API by player or level, adds them as approved runs with notes set to "Valid run". Bulk approve approves pending FEDL submissions only. Full imports run one page at a time to limit rate usage.</p>
|
||||
</div>
|
||||
<div class="admin-actions admin-import-actions">
|
||||
<button id="import-pointercrate" type="button" class="btn ghost-btn">Import Pointercrate</button>
|
||||
|
||||
@@ -1855,6 +1855,7 @@
|
||||
normalizePositions();
|
||||
renderAdminTable();
|
||||
setStatus('Connected to live list data.');
|
||||
updateStats();
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setStatus('Could not load list data.', true);
|
||||
@@ -1866,6 +1867,7 @@
|
||||
runs = loadedRuns.slice().sort((a,b)=>new Date(b.submittedAt) - new Date(a.submittedAt));
|
||||
renderRunsTable();
|
||||
setRunsStatus('Connected to live run submissions.');
|
||||
updateStats();
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setRunsStatus('Could not load run submissions.', true);
|
||||
@@ -2284,6 +2286,233 @@
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
});
|
||||
}
|
||||
|
||||
const bugReportsBody = qs('bug-reports-body');
|
||||
const bugReportSearchEl = qs('bug-report-search');
|
||||
const bugReportsStatusEl = qs('bug-reports-admin-status');
|
||||
let bugReports = [];
|
||||
|
||||
function setBugReportsStatus(message, isError){
|
||||
if(!bugReportsStatusEl) return;
|
||||
bugReportsStatusEl.textContent = message;
|
||||
bugReportsStatusEl.classList.toggle('error-text', !!isError);
|
||||
}
|
||||
|
||||
function loadBugReports(){
|
||||
if(!canUseLiveServer){
|
||||
setBugReportsStatus('Start the Node server to load bug reports.', true);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return fetch(`${liveServerBase}/api/bugreports`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders()
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Failed to load bug reports');
|
||||
return r.json();
|
||||
}).then(payload=>{
|
||||
return Array.isArray(payload.items) ? payload.items : [];
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setBugReportsStatus('Could not load bug reports.', true);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function refreshBugReports(){
|
||||
return loadBugReports().then(loaded=>{
|
||||
bugReports = loaded.slice().sort((a,b)=>new Date(b.submittedAt) - new Date(a.submittedAt));
|
||||
renderBugReportsTable();
|
||||
updateStats();
|
||||
});
|
||||
}
|
||||
|
||||
function filteredBugReports(){
|
||||
const query = (bugReportSearchEl && bugReportSearchEl.value || '').trim().toLowerCase();
|
||||
if(!query) return bugReports;
|
||||
return bugReports.filter(report=>{
|
||||
return [
|
||||
report.category,
|
||||
report.status,
|
||||
report.subject,
|
||||
report.description,
|
||||
report.accountUsername
|
||||
].some(value=>String(value || '').toLowerCase().includes(query));
|
||||
});
|
||||
}
|
||||
|
||||
function renderBugReportsTable(){
|
||||
const rows = filteredBugReports();
|
||||
if(!bugReportsBody) return;
|
||||
bugReportsBody.innerHTML = '';
|
||||
rows.forEach(report=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><span class="status-pill status-${escapeAttr(report.category || 'other')}">${escapeHtml(report.category || 'other')}</span></td>
|
||||
<td><span class="status-pill status-${escapeAttr(report.status || 'open')}">${escapeHtml(report.status || 'open')}</span></td>
|
||||
<td><strong>${escapeHtml(report.accountUsername || 'Anonymous')}</strong></td>
|
||||
<td>
|
||||
<div class="run-admin-cell">
|
||||
<strong>${escapeHtml(report.subject || 'Untitled')}</strong>
|
||||
<span class="muted small">${escapeHtml((report.description || '').slice(0, 100))}${report.description && report.description.length > 100 ? '...' : ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${escapeHtml(formatDate(report.submittedAt))}</td>
|
||||
<td>
|
||||
<div class="run-admin-actions">
|
||||
<button type="button" class="btn ghost-btn small-btn" data-bug-action="resolved" data-bug-id="${escapeAttr(report.id)}">Resolve</button>
|
||||
<button type="button" class="btn danger-btn small-btn" data-bug-delete="${escapeAttr(report.id)}">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
bugReportsBody.appendChild(tr);
|
||||
});
|
||||
if(!rows.length){
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td colspan="6" class="muted">No bug reports match your search.</td>';
|
||||
bugReportsBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBugReportStatus(reportId, status){
|
||||
if(!canUseLiveServer){
|
||||
setBugReportsStatus('Start the Node server to update bug reports.', true);
|
||||
return;
|
||||
}
|
||||
const report = bugReports.find(entry=>entry.id === reportId);
|
||||
if(!report) return;
|
||||
fetch(`${liveServerBase}/api/bugreports/${encodeURIComponent(reportId)}`, {
|
||||
method:'PUT',
|
||||
headers:authHeaders({'Content-Type':'application/json'}),
|
||||
body: JSON.stringify({
|
||||
...report,
|
||||
status
|
||||
})
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Bug report update failed');
|
||||
return refreshBugReports();
|
||||
}).then(()=>{
|
||||
setBugReportsStatus(`Bug report marked ${status}.`);
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
if(String(err && err.message || '') === 'Admin auth failed'){
|
||||
handleAdminAuthFailure('Wrong admin password. Enter it above to update bug reports.', setBugReportsStatus);
|
||||
return;
|
||||
}
|
||||
setBugReportsStatus('Could not update that bug report.', true);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteBugReport(reportId){
|
||||
if(!canUseLiveServer){
|
||||
setBugReportsStatus('Start the Node server to delete bug reports.', true);
|
||||
return;
|
||||
}
|
||||
fetch(`${liveServerBase}/api/bugreports/${encodeURIComponent(reportId)}`, {
|
||||
method:'DELETE',
|
||||
headers:authHeaders()
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Bug report delete failed');
|
||||
return refreshBugReports();
|
||||
}).then(()=>{
|
||||
setBugReportsStatus('Bug report removed.');
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
if(String(err && err.message || '') === 'Admin auth failed'){
|
||||
handleAdminAuthFailure('Wrong admin password. Enter it above to delete bug reports.', setBugReportsStatus);
|
||||
return;
|
||||
}
|
||||
setBugReportsStatus('Could not delete that bug report.', true);
|
||||
});
|
||||
}
|
||||
|
||||
if(bugReportsBody){
|
||||
bugReportsBody.addEventListener('click', function(event){
|
||||
const actionButton = event.target.closest('[data-bug-action]');
|
||||
if(actionButton){
|
||||
updateBugReportStatus(
|
||||
actionButton.getAttribute('data-bug-id'),
|
||||
actionButton.getAttribute('data-bug-action')
|
||||
);
|
||||
return;
|
||||
}
|
||||
const deleteButton = event.target.closest('[data-bug-delete]');
|
||||
if(deleteButton){
|
||||
const reportId = deleteButton.getAttribute('data-bug-delete');
|
||||
if(reportId && window.confirm('Delete this bug report?')){
|
||||
deleteBugReport(reportId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(bugReportSearchEl){
|
||||
bugReportSearchEl.addEventListener('input', renderBugReportsTable);
|
||||
}
|
||||
|
||||
function updateStats(){
|
||||
const totalRuns = Array.isArray(runs) ? runs.length : 0;
|
||||
const pendingRuns = Array.isArray(runs) ? runs.filter(r=>String(r.status || '').toLowerCase() === 'pending').length : 0;
|
||||
const levelsCount = items ? items.filter(i=>i && i.title && !i._isDraft).length : 0;
|
||||
const openReports = Array.isArray(bugReports) ? bugReports.filter(r=>String(r.status || '').toLowerCase() === 'open').length : 0;
|
||||
const statTotalRuns = qs('stat-total-runs');
|
||||
const statPendingRuns = qs('stat-pending-runs');
|
||||
const statLevelsCount = qs('stat-levels-count');
|
||||
const statBugReports = qs('stat-bug-reports');
|
||||
const statsStatusEl = qs('admin-stats-status');
|
||||
if(statTotalRuns) statTotalRuns.textContent = totalRuns;
|
||||
if(statPendingRuns) statPendingRuns.textContent = pendingRuns;
|
||||
if(statLevelsCount) statLevelsCount.textContent = levelsCount;
|
||||
if(statBugReports) statBugReports.textContent = openReports;
|
||||
if(statsStatusEl) statsStatusEl.textContent = 'Stats loaded.';
|
||||
}
|
||||
|
||||
if(bugReportsBody || qs('stat-total-runs')){
|
||||
refreshBugReports().then(()=>{
|
||||
setBugReportsStatus('Bug reports loaded.');
|
||||
});
|
||||
}
|
||||
|
||||
onLiveUpdate(function(updatedItems){
|
||||
items = updatedItems.slice().sort((a,b)=>(Number(a.position) || 0) - (Number(b.position) || 0)).map(item=>({
|
||||
level: item.level,
|
||||
position: item.position,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
_isDraft: false
|
||||
}));
|
||||
normalizePositions();
|
||||
renderAdminTable();
|
||||
setStatus('List reloaded from live server.');
|
||||
updateStats();
|
||||
});
|
||||
onRunsUpdate(function(updatedRuns){
|
||||
runs = updatedRuns.slice().sort((a,b)=>new Date(b.submittedAt) - new Date(a.submittedAt));
|
||||
renderRunsTable();
|
||||
setRunsStatus('Run queue reloaded from the live server.');
|
||||
updateStats();
|
||||
});
|
||||
|
||||
const adminTabButtons = document.querySelectorAll('.admin-tab');
|
||||
const adminTabPanels = document.querySelectorAll('.admin-tab-panel');
|
||||
|
||||
function switchAdminTab(tabName){
|
||||
adminTabButtons.forEach(btn=>{
|
||||
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
|
||||
});
|
||||
adminTabPanels.forEach(panel=>{
|
||||
panel.hidden = panel.id !== `tab-${tabName}`;
|
||||
});
|
||||
}
|
||||
|
||||
adminTabButtons.forEach(btn=>{
|
||||
btn.addEventListener('click', ()=>{
|
||||
const tab = btn.getAttribute('data-tab');
|
||||
if(tab) switchAdminTab(tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(page==='run'){
|
||||
@@ -2648,6 +2877,70 @@
|
||||
});
|
||||
}
|
||||
|
||||
if(page==='contact'){
|
||||
const form = qs('contact-form');
|
||||
const formStatusEl = qs('contact-form-status');
|
||||
const categoryEl = qs('contact-category');
|
||||
const subjectEl = qs('contact-subject');
|
||||
const descriptionEl = qs('contact-description');
|
||||
const emailEl = qs('contact-email');
|
||||
|
||||
function setContactFormStatus(message, isError){
|
||||
if(!formStatusEl) return;
|
||||
formStatusEl.textContent = message;
|
||||
if(isError){
|
||||
formStatusEl.classList.add('error-text');
|
||||
formStatusEl.classList.remove('success-text');
|
||||
}else{
|
||||
formStatusEl.classList.remove('error-text');
|
||||
formStatusEl.classList.add('success-text');
|
||||
}
|
||||
}
|
||||
|
||||
if(form){
|
||||
form.addEventListener('submit', function(event){
|
||||
event.preventDefault();
|
||||
if(!canUseLiveServer){
|
||||
setContactFormStatus('Start the Node server before submitting.', true);
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
category: categoryEl ? categoryEl.value : 'other',
|
||||
subject: subjectEl ? subjectEl.value.trim() : '',
|
||||
description: descriptionEl ? descriptionEl.value.trim() : '',
|
||||
email: emailEl ? emailEl.value.trim() : ''
|
||||
};
|
||||
if(!payload.subject || !payload.description){
|
||||
setContactFormStatus('Subject and description are required.', true);
|
||||
return;
|
||||
}
|
||||
setContactFormStatus('Submitting your report...');
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const tok = fedlGetAuthToken();
|
||||
if(tok){
|
||||
headers.Authorization = `Bearer ${tok}`;
|
||||
}
|
||||
fetch(`${liveServerBase}/api/bugreports`, {
|
||||
method:'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
}).then(async r=>{
|
||||
if(!r.ok){
|
||||
const { message } = await fedlReadJsonResponse(r);
|
||||
throw new Error(message || 'Submit failed');
|
||||
}
|
||||
return r.json();
|
||||
}).then(()=>{
|
||||
setContactFormStatus('Thank you! Your report has been submitted. The admins will review it soon.');
|
||||
if(form) form.reset();
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setContactFormStatus(err.message || 'Could not submit your report. Try again later.', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
injectFedlAuthNav();
|
||||
fedlRefreshAuthState().finally(()=>{
|
||||
fedlUpdateAuthNav();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Contact Us - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body data-page="contact">
|
||||
<header>
|
||||
<h1>Contact Us</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="guess.html">Guess Game</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="run-shell">
|
||||
<section class="panel run-panel">
|
||||
<div class="run-head">
|
||||
<div>
|
||||
<p class="hero-kicker">FEDL Contact</p>
|
||||
<h2>Report a bug or send feedback</h2>
|
||||
<p class="muted run-intro">Found a bug? Have a suggestion? Fill out the form below and the admins will review it. If you <a href="login.html">log in</a>, your username will be attached to the report.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="contact-form" class="run-form">
|
||||
<label class="control-block" for="contact-category">
|
||||
<span>Category</span>
|
||||
<select id="contact-category" name="category" required>
|
||||
<option value="bug">Bug Report</option>
|
||||
<option value="suggestion">Suggestion</option>
|
||||
<option value="question">Question</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-subject">
|
||||
<span>Subject</span>
|
||||
<input id="contact-subject" name="subject" type="text" placeholder="Brief summary of the issue" required />
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-description">
|
||||
<span>Description</span>
|
||||
<textarea id="contact-description" name="description" rows="8" placeholder="Describe the bug, include steps to reproduce if applicable, and any screenshots or links that might help." required></textarea>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-email">
|
||||
<span>Email (optional)</span>
|
||||
<input id="contact-email" name="email" type="email" placeholder="Your email if you'd like a response" />
|
||||
</label>
|
||||
|
||||
<div class="run-actions">
|
||||
<button id="contact-submit" type="submit" class="btn">Submit Report</button>
|
||||
<p id="contact-form-status" class="muted">The server needs to be running for submissions.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="panel run-side-panel">
|
||||
<p class="status-label">What to report</p>
|
||||
<h2>Before you submit</h2>
|
||||
<div class="run-checklist">
|
||||
<article>
|
||||
<strong>Bug details</strong>
|
||||
<p>Include what you were doing when the bug occurred and what you expected to happen.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Steps to reproduce</strong>
|
||||
<p>List the steps needed to trigger the bug so we can try to replicate it.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Screenshots or videos</strong>
|
||||
<p>Links to screenshots or videos can help us understand the issue better.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="layout">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="auth-main">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="layout">
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
<a href="index.html">Home</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<span class="fedl-auth-nav"></span>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -11,6 +11,7 @@ const usersPath = path.join(__dirname, 'users.json');
|
||||
const sessionsPath = path.join(__dirname, 'sessions.json');
|
||||
const userDataPath = path.join(__dirname, 'userdata.json');
|
||||
const postsPath = path.join(__dirname, 'posts.json');
|
||||
const bugReportsPath = path.join(__dirname, 'bugreports.json');
|
||||
const port = Number(process.env.PORT) || 8090;
|
||||
const host = process.env.HOST || '127.0.0.1';
|
||||
const BASE = '/fedl';
|
||||
@@ -259,6 +260,23 @@ function writePosts(posts) {
|
||||
fs.writeFileSync(postsPath, `${JSON.stringify(posts, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function ensureBugReportsFile() {
|
||||
if (!fs.existsSync(bugReportsPath)) {
|
||||
fs.writeFileSync(bugReportsPath, '[]\n', 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function readBugReports() {
|
||||
ensureBugReportsFile();
|
||||
const raw = fs.readFileSync(bugReportsPath, 'utf8');
|
||||
const parsed = JSON.parse(raw || '[]');
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
|
||||
function writeBugReports(reports) {
|
||||
fs.writeFileSync(bugReportsPath, `${JSON.stringify(reports, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function normalizePost(post) {
|
||||
return {
|
||||
id: String(post.id || ''),
|
||||
@@ -1078,6 +1096,104 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && pathname === '/api/bugreports') {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
sendJson(res, 200, { items: readBugReports() });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: 'Could not read bug reports' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && pathname === '/api/bugreports') {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
if (body.length > 65536) req.destroy();
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const payload = JSON.parse(body || '{}');
|
||||
const category = String(payload.category || 'other').trim().toLowerCase();
|
||||
const subject = String(payload.subject || '').trim().slice(0, 200);
|
||||
const description = String(payload.description || '').trim().slice(0, 4000);
|
||||
const email = String(payload.email || '').trim().slice(0, 200);
|
||||
if (!subject || !description) {
|
||||
sendJson(res, 400, { error: 'Subject and description are required' });
|
||||
return;
|
||||
}
|
||||
const sess = findSession(getBearerToken(req));
|
||||
const newReport = {
|
||||
id: `bug_${Date.now().toString(36)}${crypto.randomBytes(4).toString('hex')}`,
|
||||
category,
|
||||
subject,
|
||||
description,
|
||||
email,
|
||||
submittedAt: new Date().toISOString(),
|
||||
status: 'open',
|
||||
accountUserId: sess ? sess.userId : '',
|
||||
accountUsername: sess ? sess.username : ''
|
||||
};
|
||||
const reports = readBugReports();
|
||||
reports.unshift(newReport);
|
||||
writeBugReports(reports);
|
||||
sendEvent('bugreports-update', { updatedAt: newReport.submittedAt });
|
||||
sendJson(res, 201, { ok: true, item: newReport });
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: 'Invalid bug report payload' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((req.method === 'PUT' || req.method === 'DELETE') && pathname.startsWith('/api/bugreports/')) {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
const reportId = pathname.slice('/api/bugreports/'.length);
|
||||
if (!reportId) {
|
||||
sendJson(res, 400, { error: 'Report id is required' });
|
||||
return;
|
||||
}
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
if (body.length > 65536) req.destroy();
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const reports = readBugReports();
|
||||
const index = reports.findIndex(r => r.id === reportId);
|
||||
if (index === -1) {
|
||||
sendJson(res, 404, { error: 'Report not found' });
|
||||
return;
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
reports.splice(index, 1);
|
||||
writeBugReports(reports);
|
||||
sendEvent('bugreports-update', { updatedAt: new Date().toISOString() });
|
||||
sendJson(res, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(body || '{}');
|
||||
reports[index] = {
|
||||
...reports[index],
|
||||
category: String(payload.category || reports[index].category || 'other').trim().toLowerCase(),
|
||||
subject: String(payload.subject || reports[index].subject || '').trim().slice(0, 200),
|
||||
description: String(payload.description || reports[index].description || '').trim().slice(0, 4000),
|
||||
email: String(payload.email || reports[index].email || '').trim().slice(0, 200),
|
||||
status: String(payload.status || reports[index].status || 'open').trim().toLowerCase(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
writeBugReports(reports);
|
||||
sendEvent('bugreports-update', { updatedAt: reports[index].updatedAt });
|
||||
sendJson(res, 200, { ok: true, item: reports[index] });
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: 'Invalid bug report update payload' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && pathname === '/api/list') {
|
||||
try {
|
||||
const text = readDataText();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="post.html">Posts</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="auth-main">
|
||||
|
||||
+13
@@ -354,3 +354,16 @@ header nav .fedl-auth-nav a:hover{color:var(--text)}
|
||||
@media(max-width:900px){.error-grid{grid-template-columns:1fr}.error-copy,.error-side{padding:24px 20px}.error-copy h2{font-size:2.5rem}}
|
||||
@media(max-width:900px){.admin-overview-panel,.admin-toolbar{flex-direction:column;align-items:stretch}.admin-actions{flex-direction:column;align-items:stretch}.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:0;width:100%}.run-shell{grid-template-columns:1fr}.run-head{flex-direction:column;align-items:flex-start}.run-form{grid-template-columns:1fr}.admin-login-screen{padding:18px}.admin-login-card{padding:26px 22px}}
|
||||
@media(max-width:640px){header{flex-direction:column;align-items:flex-start;gap:12px}nav{display:flex;flex-wrap:wrap;gap:10px}nav a{margin-left:0}header nav a.btn{margin-left:0}.home-copy h2{max-width:none}.hero-actions{flex-direction:column;align-items:stretch}.hero-actions .btn,.roulette-btn{width:100%;text-align:center}.list-hero-panel,.list-controls-panel,.list-table-panel,.roulette-panel,.roulette-rules-card,.run-panel,.run-side-panel,.submission-panel{padding:22px 18px}.table-hint{text-align:left}.run-admin-actions{justify-content:flex-start}}
|
||||
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-top:16px}
|
||||
.stat-box{padding:16px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);text-align:center}
|
||||
.stat-box strong{display:block;margin-bottom:6px;font-size:1.75rem;color:var(--accent)}
|
||||
.stat-box span{display:block;color:var(--muted);font-size:.9rem}
|
||||
.bug-report-detail-row td{padding-top:0}
|
||||
.bug-report-detail{display:grid;gap:8px;padding:0 0 14px 0;color:#dce8f5}
|
||||
.bug-report-detail a{color:var(--accent)}
|
||||
.admin-tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.admin-tab{padding:10px 18px;border-radius:999px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:var(--muted);cursor:pointer;font-weight:700;transition:all .2s ease}
|
||||
.admin-tab:hover{background:rgba(255,255,255,0.08);color:var(--text)}
|
||||
.admin-tab.active{background:var(--accent);color:#042}
|
||||
.admin-tab-panel{display:block}
|
||||
.admin-tab-panel[hidden]{display:none}
|
||||
|
||||
Reference in New Issue
Block a user