461 lines
20 KiB
JavaScript
461 lines
20 KiB
JavaScript
/* ===================================================
|
|
JAESWIFT — Admin Panel JS
|
|
=================================================== */
|
|
const AdminApp = (() => {
|
|
const API = window.location.hostname === 'localhost' ? 'http://localhost:5000' : '/api';
|
|
let token = localStorage.getItem('jaeswift_token') || '';
|
|
|
|
// ─── Auth ───
|
|
function headers() {
|
|
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
|
}
|
|
|
|
async function login(e) {
|
|
e.preventDefault();
|
|
const user = document.getElementById('loginUser').value;
|
|
const pass = document.getElementById('loginPass').value;
|
|
const errEl = document.getElementById('loginError');
|
|
errEl.textContent = '';
|
|
try {
|
|
const r = await fetch(API + '/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: user, password: pass })
|
|
});
|
|
const d = await r.json();
|
|
if (d.token) {
|
|
token = d.token;
|
|
localStorage.setItem('jaeswift_token', token);
|
|
showApp();
|
|
} else {
|
|
errEl.textContent = 'ACCESS DENIED: ' + (d.error || 'Invalid credentials');
|
|
}
|
|
} catch {
|
|
errEl.textContent = 'CONNECTION FAILED';
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
token = '';
|
|
localStorage.removeItem('jaeswift_token');
|
|
document.getElementById('adminApp').style.display = 'none';
|
|
document.getElementById('loginScreen').style.display = '';
|
|
}
|
|
|
|
async function checkAuth() {
|
|
if (!token) return false;
|
|
try {
|
|
const r = await fetch(API + '/auth/check', { headers: headers() });
|
|
const d = await r.json();
|
|
return d.valid === true;
|
|
} catch { return false; }
|
|
}
|
|
|
|
async function showApp() {
|
|
document.getElementById('loginScreen').style.display = 'none';
|
|
document.getElementById('adminApp').style.display = '';
|
|
loadDashboard();
|
|
}
|
|
|
|
// ─── Section Switching ───
|
|
function showSection(name) {
|
|
document.querySelectorAll('.admin-section').forEach(s => s.style.display = 'none');
|
|
const target = document.getElementById('section-' + name);
|
|
if (target) target.style.display = '';
|
|
|
|
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
|
const link = document.querySelector('.sidebar-link[data-section="' + name + '"]');
|
|
if (link) link.classList.add('active');
|
|
|
|
const titles = { dashboard: 'DASHBOARD', posts: 'POSTS', editor: 'EDITOR', tracks: 'TRACKS', settings: 'SETTINGS' };
|
|
document.getElementById('topbarSection').textContent = titles[name] || name.toUpperCase();
|
|
|
|
if (name === 'dashboard') loadDashboard();
|
|
if (name === 'posts') loadPosts();
|
|
if (name === 'tracks') loadTracks();
|
|
if (name === 'settings') loadSettings();
|
|
if (name === 'editor') {
|
|
if (!document.getElementById('editSlugOriginal').value) {
|
|
document.getElementById('editorHeading').textContent = 'NEW TRANSMISSION';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Dashboard ───
|
|
async function loadDashboard() {
|
|
try {
|
|
const [posts, tracks, stats, services, threats] = await Promise.all([
|
|
fetch(API + '/posts').then(r => r.json()),
|
|
fetch(API + '/nowplaying').then(r => r.json()).catch(() => null),
|
|
fetch(API + '/stats').then(r => r.json()),
|
|
fetch(API + '/services').then(r => r.json()).catch(() => []),
|
|
fetch(API + '/threats').then(r => r.json()).catch(() => []),
|
|
]);
|
|
|
|
// Fetch tracks count
|
|
let trackCount = '--';
|
|
try {
|
|
const tr = await fetch(API + '/tracks');
|
|
if (tr.ok) { trackCount = (await tr.json()).length; }
|
|
} catch {
|
|
// tracks endpoint might not exist, try loading from data
|
|
trackCount = '~35';
|
|
}
|
|
|
|
document.getElementById('dashPosts').textContent = posts.length;
|
|
document.getElementById('dashWords').textContent = posts.reduce((sum, p) => sum + (p.word_count || 0), 0).toLocaleString();
|
|
document.getElementById('dashTracks').textContent = trackCount;
|
|
document.getElementById('dashCPU').textContent = Math.round(stats.cpu_percent) + '%';
|
|
document.getElementById('dashMem').textContent = Math.round(stats.memory_percent) + '%';
|
|
document.getElementById('dashDisk').textContent = stats.disk_percent + '%';
|
|
|
|
// Services
|
|
const svcEl = document.getElementById('dashServices');
|
|
svcEl.innerHTML = services.map(s => `
|
|
<div class="service-card ${s.status}">
|
|
<span class="service-dot">${s.status === 'online' ? '●' : '○'}</span>
|
|
<span class="service-name">${s.name}</span>
|
|
<span class="service-ms">${s.response_time_ms}ms</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Threats
|
|
const threatEl = document.getElementById('dashThreats');
|
|
threatEl.innerHTML = threats.length ? threats.map(t => `
|
|
<div class="threat-row">
|
|
<span class="threat-id">${t.id}</span>
|
|
<span class="threat-summary">${t.summary}</span>
|
|
<span class="threat-cvss cvss-${t.cvss >= 9 ? 'crit' : t.cvss >= 7 ? 'high' : t.cvss >= 4 ? 'med' : 'low'}">${t.cvss || '?'}</span>
|
|
</div>
|
|
`).join('') : '<span class="no-data">No threats detected</span>';
|
|
|
|
} catch (e) {
|
|
console.error('Dashboard load error:', e);
|
|
}
|
|
}
|
|
|
|
// ─── Posts ───
|
|
async function loadPosts() {
|
|
const posts = await fetch(API + '/posts').then(r => r.json());
|
|
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
const tbody = document.getElementById('postsTableBody');
|
|
tbody.innerHTML = posts.map(p => `
|
|
<tr>
|
|
<td>${p.date || '--'}</td>
|
|
<td><a href="post.html?slug=${p.slug}" target="_blank" class="post-link">${p.title}</a></td>
|
|
<td>${(p.tags || []).map(t => '<span class="tag-pill">' + t + '</span>').join(' ')}</td>
|
|
<td>${p.word_count || 0}</td>
|
|
<td><span class="threat-badge threat-${p.threat_level || 'low'}">${(p.threat_level || 'low').toUpperCase()}</span></td>
|
|
<td class="actions-cell">
|
|
<button class="action-sm edit" onclick="AdminApp.editPost('${p.slug}')">EDIT</button>
|
|
<button class="action-sm delete" onclick="AdminApp.deletePost('${p.slug}')">DEL</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function editPost(slug) {
|
|
const post = await fetch(API + '/posts/' + slug).then(r => r.json());
|
|
document.getElementById('editSlugOriginal').value = slug;
|
|
document.getElementById('edTitle').value = post.title || '';
|
|
document.getElementById('edSlug').value = post.slug || '';
|
|
document.getElementById('edDate').value = post.date || '';
|
|
document.getElementById('edTime').value = post.time || '00:00';
|
|
document.getElementById('edTags').value = (post.tags || []).join(', ');
|
|
document.getElementById('edExcerpt').value = post.excerpt || '';
|
|
document.getElementById('edContent').value = post.content || '';
|
|
document.getElementById('edMood').value = post.mood || 'focused';
|
|
document.getElementById('edEnergy').value = post.energy || 3;
|
|
document.getElementById('edEnergyVal').textContent = post.energy || 3;
|
|
document.getElementById('edMotivation').value = post.motivation || 3;
|
|
document.getElementById('edMotivationVal').textContent = post.motivation || 3;
|
|
document.getElementById('edFocus').value = post.focus || 3;
|
|
document.getElementById('edFocusVal').textContent = post.focus || 3;
|
|
document.getElementById('edDifficulty').value = post.difficulty || 3;
|
|
document.getElementById('edDifficultyVal').textContent = post.difficulty || 3;
|
|
document.getElementById('edCoffee').value = post.coffee || 2;
|
|
document.getElementById('edBPM').value = post.bpm || post.heart_rate || 72;
|
|
document.getElementById('edThreat').value = post.threat_level || 'low';
|
|
document.getElementById('editorHeading').textContent = 'EDIT TRANSMISSION';
|
|
updateWordCount();
|
|
updatePreview();
|
|
showSection('editor');
|
|
}
|
|
|
|
async function savePost(e) {
|
|
e.preventDefault();
|
|
const slugOrig = document.getElementById('editSlugOriginal').value;
|
|
const title = document.getElementById('edTitle').value.trim();
|
|
const slug = document.getElementById('edSlug').value.trim() || title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
const data = {
|
|
title,
|
|
slug,
|
|
date: document.getElementById('edDate').value || new Date().toISOString().split('T')[0],
|
|
time: document.getElementById('edTime').value || '00:00',
|
|
time_written: document.getElementById('edTime').value || '00:00',
|
|
tags: document.getElementById('edTags').value.split(',').map(t => t.trim()).filter(Boolean),
|
|
excerpt: document.getElementById('edExcerpt').value.trim(),
|
|
content: document.getElementById('edContent').value,
|
|
mood: document.getElementById('edMood').value,
|
|
energy: parseInt(document.getElementById('edEnergy').value),
|
|
motivation: parseInt(document.getElementById('edMotivation').value),
|
|
focus: parseInt(document.getElementById('edFocus').value),
|
|
difficulty: parseInt(document.getElementById('edDifficulty').value),
|
|
coffee: parseInt(document.getElementById('edCoffee').value),
|
|
bpm: parseInt(document.getElementById('edBPM').value),
|
|
heart_rate: parseInt(document.getElementById('edBPM').value),
|
|
threat_level: document.getElementById('edThreat').value,
|
|
};
|
|
|
|
const method = slugOrig ? 'PUT' : 'POST';
|
|
const url = slugOrig ? API + '/posts/' + slugOrig : API + '/posts';
|
|
|
|
try {
|
|
const r = await fetch(url, { method, headers: headers(), body: JSON.stringify(data) });
|
|
if (r.ok) {
|
|
clearEditor();
|
|
showSection('posts');
|
|
showNotification('TRANSMISSION ' + (slugOrig ? 'UPDATED' : 'SENT'));
|
|
} else {
|
|
const err = await r.json();
|
|
showNotification('ERROR: ' + (err.message || r.status), 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('TRANSMISSION FAILED: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deletePost(slug) {
|
|
if (!confirm('Delete post "' + slug + '"? This cannot be undone.')) return;
|
|
const r = await fetch(API + '/posts/' + slug, { method: 'DELETE', headers: headers() });
|
|
if (r.ok) {
|
|
loadPosts();
|
|
showNotification('POST DELETED');
|
|
}
|
|
}
|
|
|
|
function clearEditor() {
|
|
document.getElementById('editSlugOriginal').value = '';
|
|
document.getElementById('editorForm').reset();
|
|
document.getElementById('edEnergyVal').textContent = '3';
|
|
document.getElementById('edMotivationVal').textContent = '3';
|
|
document.getElementById('edFocusVal').textContent = '3';
|
|
document.getElementById('edDifficultyVal').textContent = '3';
|
|
document.getElementById('edWordCount').textContent = '0 words';
|
|
document.getElementById('edPreview').innerHTML = '';
|
|
document.getElementById('editorHeading').textContent = 'NEW TRANSMISSION';
|
|
}
|
|
|
|
// ─── Markdown Helpers ───
|
|
function insertMD(before, after) {
|
|
const ta = document.getElementById('edContent');
|
|
const start = ta.selectionStart;
|
|
const end = ta.selectionEnd;
|
|
const sel = ta.value.substring(start, end);
|
|
ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
|
|
ta.focus();
|
|
ta.selectionStart = start + before.length;
|
|
ta.selectionEnd = start + before.length + sel.length;
|
|
updateWordCount();
|
|
updatePreview();
|
|
}
|
|
|
|
function updateWordCount() {
|
|
const text = document.getElementById('edContent').value.trim();
|
|
const count = text ? text.split(/\s+/).length : 0;
|
|
document.getElementById('edWordCount').textContent = count + ' words';
|
|
}
|
|
|
|
function updatePreview() {
|
|
const md = document.getElementById('edContent').value;
|
|
document.getElementById('edPreview').innerHTML = renderMarkdown(md);
|
|
}
|
|
|
|
function renderMarkdown(md) {
|
|
let html = md
|
|
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
|
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
|
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
.replace(/^---$/gm, '<hr>')
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/\n/g, '<br>');
|
|
return '<div class="post-rendered"><p>' + html + '</p></div>';
|
|
}
|
|
|
|
// ─── Tracks ───
|
|
async function loadTracks() {
|
|
try {
|
|
// Try fetching from tracks API endpoint
|
|
let tracks;
|
|
try {
|
|
const r = await fetch(API + '/tracks');
|
|
if (r.ok) {
|
|
tracks = await r.json();
|
|
} else throw new Error();
|
|
} catch {
|
|
// Fallback: load from data file directly
|
|
tracks = [];
|
|
}
|
|
|
|
const list = document.getElementById('tracksList');
|
|
if (!tracks.length) {
|
|
list.innerHTML = '<span class="no-data">No tracks loaded — API endpoint /api/tracks may need adding.</span>';
|
|
return;
|
|
}
|
|
list.innerHTML = tracks.map((t, i) => `
|
|
<div class="track-row">
|
|
<span class="track-num">${String(i + 1).padStart(2, '0')}</span>
|
|
<span class="track-title">${t.track}</span>
|
|
<span class="track-artist">${t.artist}</span>
|
|
<span class="track-album">${t.album || ''}</span>
|
|
<button class="action-sm delete" onclick="AdminApp.deleteTrack(${i})">✕</button>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Track load error:', e);
|
|
}
|
|
}
|
|
|
|
function showAddTrack() {
|
|
const f = document.getElementById('trackAddForm');
|
|
f.style.display = f.style.display === 'none' ? 'flex' : 'none';
|
|
}
|
|
|
|
async function addTrack() {
|
|
const artist = document.getElementById('trackArtist').value.trim();
|
|
const track = document.getElementById('trackTitle').value.trim();
|
|
const album = document.getElementById('trackAlbum').value.trim();
|
|
if (!artist || !track) return;
|
|
|
|
try {
|
|
const r = await fetch(API + '/tracks', {
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify({ artist, track, album })
|
|
});
|
|
if (r.ok) {
|
|
document.getElementById('trackArtist').value = '';
|
|
document.getElementById('trackTitle').value = '';
|
|
document.getElementById('trackAlbum').value = '';
|
|
loadTracks();
|
|
showNotification('TRACK ADDED');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Failed to add track', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteTrack(index) {
|
|
try {
|
|
const r = await fetch(API + '/tracks/' + index, { method: 'DELETE', headers: headers() });
|
|
if (r.ok) loadTracks();
|
|
} catch {}
|
|
}
|
|
|
|
// ─── Settings ───
|
|
async function loadSettings() {
|
|
try {
|
|
const settings = await fetch(API + '/settings').then(r => r.json());
|
|
if (settings.weather !== undefined) document.getElementById('setWeather').checked = settings.weather;
|
|
if (settings.nowplaying !== undefined) document.getElementById('setNowPlaying').checked = settings.nowplaying;
|
|
if (settings.threats !== undefined) document.getElementById('setThreats').checked = settings.threats;
|
|
if (settings.terminal !== undefined) document.getElementById('setTerminal').checked = settings.terminal;
|
|
} catch {}
|
|
}
|
|
|
|
async function saveSettings(e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
weather: document.getElementById('setWeather').checked,
|
|
nowplaying: document.getElementById('setNowPlaying').checked,
|
|
threats: document.getElementById('setThreats').checked,
|
|
terminal: document.getElementById('setTerminal').checked,
|
|
};
|
|
await fetch(API + '/settings', { method: 'PUT', headers: headers(), body: JSON.stringify(data) });
|
|
showNotification('SETTINGS SAVED');
|
|
}
|
|
|
|
// ─── Notifications ───
|
|
function showNotification(msg, type = 'success') {
|
|
let notif = document.querySelector('.admin-notification');
|
|
if (!notif) {
|
|
notif = document.createElement('div');
|
|
notif.className = 'admin-notification';
|
|
document.body.appendChild(notif);
|
|
}
|
|
notif.textContent = msg;
|
|
notif.className = 'admin-notification ' + type + ' show';
|
|
setTimeout(() => notif.classList.remove('show'), 3000);
|
|
}
|
|
|
|
// ─── Init ───
|
|
async function init() {
|
|
// Check if already authed
|
|
if (await checkAuth()) {
|
|
showApp();
|
|
}
|
|
|
|
// Login form
|
|
document.getElementById('loginForm').addEventListener('submit', login);
|
|
|
|
// Editor form
|
|
document.getElementById('editorForm').addEventListener('submit', savePost);
|
|
|
|
// Settings form
|
|
document.getElementById('settingsForm').addEventListener('submit', saveSettings);
|
|
|
|
// Auto slug from title
|
|
document.getElementById('edTitle').addEventListener('input', (e) => {
|
|
const slugField = document.getElementById('edSlug');
|
|
if (!document.getElementById('editSlugOriginal').value) {
|
|
slugField.value = e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
}
|
|
});
|
|
|
|
// Content updates
|
|
document.getElementById('edContent').addEventListener('input', () => {
|
|
updateWordCount();
|
|
updatePreview();
|
|
});
|
|
|
|
// Energy range display
|
|
document.getElementById('edEnergy').addEventListener('input', (e) => {
|
|
document.getElementById('edEnergyVal').textContent = e.target.value;
|
|
});
|
|
document.getElementById('edMotivation').addEventListener('input', (e) => {
|
|
document.getElementById('edMotivationVal').textContent = e.target.value;
|
|
});
|
|
document.getElementById('edFocus').addEventListener('input', (e) => {
|
|
document.getElementById('edFocusVal').textContent = e.target.value;
|
|
});
|
|
document.getElementById('edDifficulty').addEventListener('input', (e) => {
|
|
document.getElementById('edDifficultyVal').textContent = e.target.value;
|
|
});
|
|
|
|
// Set today's date default
|
|
document.getElementById('edDate').value = new Date().toISOString().split('T')[0];
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
// Public API
|
|
return {
|
|
showSection,
|
|
logout,
|
|
editPost,
|
|
deletePost,
|
|
clearEditor,
|
|
insertMD,
|
|
showAddTrack,
|
|
addTrack,
|
|
deleteTrack,
|
|
};
|
|
})();
|