jaeswift-website/js/admin.js

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,
};
})();