/* ═══════════════════════════════════════════════════════════════
* JAESWIFT.XYZ — Admin Panel Controller
* Complete admin application object
* ═══════════════════════════════════════════════════════════════ */
const AdminApp = {
token: localStorage.getItem('admin_token'),
API: '/api',
currentSection: 'section-dashboard',
homepageData: null,
servicesData: [],
navData: [],
linksData: [],
themeDefaults: {
accent: '#00ffc8',
bg: '#0a0e17',
text: '#e0e0e0',
scanlines: true,
particles: true,
glitch: false,
grid: true,
fontSize: 16
},
/* ───────────────────────── AUTH ───────────────────────── */
init() {
// Login form
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', (e) => this.login(e));
}
// Check auth state
if (this.checkAuth()) {
this.showApp();
this.loadDashboard();
} else {
this.showLogin();
}
// Mobile hamburger
const mobileBtn = document.getElementById('mobileMenuBtn');
if (mobileBtn) {
mobileBtn.addEventListener('click', () => this.toggleSidebar());
}
// Sidebar overlay close
const overlay = document.getElementById('sidebarOverlay');
if (overlay) {
overlay.addEventListener('click', () => this.toggleSidebar());
}
// Editor: auto-slug on title input
const editTitle = document.getElementById('editTitle');
if (editTitle) {
editTitle.addEventListener('input', () => this.autoSlug());
}
// Editor: live preview on content input
const editContent = document.getElementById('editContent');
if (editContent) {
editContent.addEventListener('input', () => this.updatePreview());
}
// Editor toolbar buttons
document.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', () => {
this.insertMarkdown(btn.getAttribute('data-action'));
});
});
// Operator HUD sliders
['editEnergy', 'editMotivation', 'editFocus', 'editDifficulty'].forEach(id => {
const slider = document.getElementById(id);
if (slider) {
const valId = 'val' + id.replace('edit', '');
const valEl = document.getElementById(valId);
if (valEl) valEl.textContent = slider.value;
slider.addEventListener('input', () => {
if (valEl) valEl.textContent = slider.value;
});
}
});
// Theme colour sync
['Accent', 'Bg', 'Text'].forEach(name => {
const picker = document.getElementById('theme' + name);
const hex = document.getElementById('theme' + name + 'Hex');
if (picker && hex) {
picker.addEventListener('input', () => {
hex.value = picker.value;
});
hex.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(hex.value)) {
picker.value = hex.value;
}
});
}
});
// Theme font size slider
const fontSlider = document.getElementById('themeFontSize');
const valFont = document.getElementById('valFontSize');
if (fontSlider && valFont) {
valFont.textContent = fontSlider.value;
fontSlider.addEventListener('input', () => {
valFont.textContent = fontSlider.value;
});
}
},
async login(e) {
e.preventDefault();
const user = document.getElementById('loginUser').value.trim();
const pass = document.getElementById('loginPass').value;
const errEl = document.getElementById('loginError');
errEl.textContent = '';
try {
const res = await fetch(this.API + '/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: user, password: pass })
});
const data = await res.json();
if (res.ok && data.token) {
this.token = data.token;
localStorage.setItem('admin_token', this.token);
this.showApp();
this.loadDashboard();
} else {
errEl.textContent = data.error || 'Authentication failed';
}
} catch (err) {
errEl.textContent = 'Connection error: ' + err.message;
}
},
logout() {
this.token = null;
localStorage.removeItem('admin_token');
this.showLogin();
},
checkAuth() {
return !!this.token;
},
authHeaders() {
return {
'Authorization': 'Bearer ' + this.token,
'Content-Type': 'application/json'
};
},
showLogin() {
const login = document.getElementById('loginScreen');
const app = document.getElementById('adminApp');
if (login) login.style.display = 'flex';
if (app) app.style.display = 'none';
},
showApp() {
const login = document.getElementById('loginScreen');
const app = document.getElementById('adminApp');
if (login) login.style.display = 'none';
if (app) app.style.display = 'flex';
},
/* ─────────────────────── NAVIGATION ─────────────────────── */
showSection(name) {
// Hide all sections
document.querySelectorAll('[id^="section-"]').forEach(s => {
s.style.display = 'none';
});
// Show target
const target = document.getElementById(name);
if (target) target.style.display = 'block';
this.currentSection = name;
// Update sidebar active
document.querySelectorAll('.sidebar a, .sidebar .nav-link').forEach(a => {
a.classList.remove('active');
});
const activeLink = document.querySelector(`[onclick*="'${name}'"]`);
if (activeLink) activeLink.classList.add('active');
// Update topbar title
const titleMap = {
'section-dashboard': 'Dashboard',
'section-posts': 'Posts',
'section-editor': 'Editor',
'section-tracks': 'Tracks',
'section-settings': 'Settings',
'section-homepage': 'Homepage',
'section-services': 'Services',
'section-navigation': 'Navigation',
'section-links': 'Links',
'section-apikeys': 'API Keys',
'section-theme': 'Theme',
'section-seo': 'SEO',
'section-contact': 'Contact',
'section-backups': 'Backups'
};
const topTitle = document.getElementById('topbarTitle') || document.querySelector('.topbar-title');
if (topTitle) topTitle.textContent = titleMap[name] || name.replace('section-', '').toUpperCase();
// Load data for section
switch (name) {
case 'section-dashboard': this.loadDashboard(); break;
case 'section-posts': this.loadPosts(); break;
case 'section-tracks': this.loadTracks(); break;
case 'section-settings': this.loadSettings(); break;
case 'section-homepage': this.loadHomepage(); break;
case 'section-services': this.loadServices(); break;
case 'section-navigation': this.loadNavigation(); break;
case 'section-links': this.loadLinks(); break;
case 'section-apikeys': this.loadApiKeys(); break;
case 'section-theme': this.loadTheme(); break;
case 'section-seo': this.loadSeo(); break;
case 'section-contact': this.loadContactSettings(); break;
}
// Close mobile sidebar
const sidebar = document.querySelector('.sidebar');
if (sidebar) sidebar.classList.remove('open');
const sOverlay = document.getElementById('sidebarOverlay');
if (sOverlay) sOverlay.classList.remove('active');
},
toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar) sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('active');
},
/* ─────────────────── NOTIFICATIONS ─────────────────── */
notify(msg, type = 'success') {
const container = document.getElementById('notifications') || document.body;
const div = document.createElement('div');
div.className = 'notification notification-' + type;
div.innerHTML = `${type === 'success' ? '✓' : type === 'error' ? '✗' : 'ℹ'} ${msg} `;
// Styles
Object.assign(div.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '14px 24px',
borderRadius: '4px',
color: '#fff',
fontFamily: 'JetBrains Mono, monospace',
fontSize: '13px',
zIndex: '10000',
display: 'flex',
alignItems: 'center',
gap: '10px',
animation: 'slideIn 0.3s ease',
border: '1px solid',
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 20px rgba(0,0,0,0.5)'
});
if (type === 'success') {
div.style.background = 'rgba(0,255,200,0.15)';
div.style.borderColor = '#00ffc8';
} else if (type === 'error') {
div.style.background = 'rgba(255,50,50,0.15)';
div.style.borderColor = '#ff3232';
} else {
div.style.background = 'rgba(0,150,255,0.15)';
div.style.borderColor = '#0096ff';
}
container.appendChild(div);
setTimeout(() => {
div.style.opacity = '0';
div.style.transition = 'opacity 0.3s ease';
setTimeout(() => div.remove(), 300);
}, 3000);
},
/* ─────────────────── DASHBOARD ─────────────────── */
async loadDashboard() {
try {
const [statsRes, postsRes, tracksRes, servicesRes, threatsRes] = await Promise.all([
fetch(this.API + '/stats', { headers: this.authHeaders() }),
fetch(this.API + '/posts', { headers: this.authHeaders() }),
fetch(this.API + '/tracks', { headers: this.authHeaders() }),
fetch(this.API + '/services', { headers: this.authHeaders() }),
fetch(this.API + '/threats', { headers: this.authHeaders() })
]);
// Stats
if (statsRes.ok) {
const stats = await statsRes.json();
const cpu = document.getElementById('dashCPU');
const mem = document.getElementById('dashMEM');
const disk = document.getElementById('dashDISK');
if (cpu) cpu.textContent = (stats.cpu_percent || stats.cpu || 0) + '%';
if (mem) mem.textContent = (stats.memory_percent || stats.memory || 0) + '%';
if (disk) disk.textContent = (stats.disk_percent || stats.disk || 0) + '%';
}
// Posts count + word count
if (postsRes.ok) {
const posts = await postsRes.json();
const arr = Array.isArray(posts) ? posts : (posts.posts || []);
const dashPosts = document.getElementById('dashPosts');
const dashWords = document.getElementById('dashWords');
if (dashPosts) dashPosts.textContent = arr.length;
if (dashWords) {
const totalWords = arr.reduce((sum, p) => sum + (p.word_count || 0), 0);
dashWords.textContent = totalWords.toLocaleString();
}
}
// Tracks count
if (tracksRes.ok) {
const tracks = await tracksRes.json();
const arr = Array.isArray(tracks) ? tracks : (tracks.tracks || []);
const dashTracks = document.getElementById('dashTracks');
if (dashTracks) dashTracks.textContent = arr.length;
}
// Services grid
if (servicesRes.ok) {
const services = await servicesRes.json();
const arr = Array.isArray(services) ? services : (services.services || []);
const grid = document.getElementById('dashServicesGrid');
if (grid) {
grid.innerHTML = arr.map(svc => {
const isUp = svc.status === 'up' || svc.status === 'online' || svc.status === true;
const dotColor = isUp ? '#00ffc8' : '#ff3232';
const statusText = isUp ? 'ONLINE' : 'OFFLINE';
return `
${this.escapeHtml(svc.name || svc.service || 'Unknown')}
${statusText}
`;
}).join('');
}
}
// Threats
if (threatsRes.ok) {
const threats = await threatsRes.json();
const arr = Array.isArray(threats) ? threats : (threats.threats || threats.items || []);
const container = document.getElementById('dashThreats');
if (container) {
if (arr.length === 0) {
container.innerHTML = 'No active threats detected ✓
';
} else {
container.innerHTML = arr.map(t => {
const severity = (t.severity || t.level || 'low').toLowerCase();
const sevColor = severity === 'critical' ? '#ff0040' : severity === 'high' ? '#ff6600' : severity === 'medium' ? '#ffaa00' : '#00ffc8';
return `
${this.escapeHtml(t.cve || t.id || t.name || 'Unknown')}
${severity}
${this.escapeHtml(t.description || t.summary || '')}
`;
}).join('');
}
}
}
} catch (err) {
console.error('Dashboard load error:', err);
}
},
/* ─────────────────── POSTS ─────────────────── */
async loadPosts() {
try {
const res = await fetch(this.API + '/posts', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load posts');
const data = await res.json();
const posts = Array.isArray(data) ? data : (data.posts || []);
const tbody = document.getElementById('postsTableBody');
if (!tbody) return;
if (posts.length === 0) {
tbody.innerHTML = 'No posts yet ';
return;
}
// Sort by date descending
posts.sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
tbody.innerHTML = posts.map(p => {
const date = p.date ? new Date(p.date).toLocaleDateString('en-GB') : '—';
const words = p.word_count || 0;
return `
${this.escapeHtml(p.title || 'Untitled')}
${this.escapeHtml(p.slug || '')}
${date}
${words.toLocaleString()}
✎
✗
`;
}).join('');
} catch (err) {
console.error('Posts load error:', err);
this.notify('Failed to load posts', 'error');
}
},
newPost() {
this.clearEditor();
const heading = document.getElementById('editorTitle');
if (heading) heading.textContent = 'NEW POST';
this.showSection('section-editor');
},
async editPost(slug) {
try {
const res = await fetch(this.API + '/posts/' + encodeURIComponent(slug), { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load post');
const post = await res.json();
const heading = document.getElementById('editorTitle');
if (heading) heading.textContent = 'EDIT POST';
document.getElementById('postId').value = post.slug || slug;
document.getElementById('editTitle').value = post.title || '';
document.getElementById('editSlug').value = post.slug || '';
// Parse date and time
if (post.date) {
const dt = new Date(post.date);
const dateStr = dt.toISOString().split('T')[0];
const timeStr = dt.toTimeString().slice(0, 5);
document.getElementById('editDate').value = dateStr;
document.getElementById('editTime').value = timeStr;
} else {
document.getElementById('editDate').value = '';
document.getElementById('editTime').value = '';
}
document.getElementById('editTags').value = Array.isArray(post.tags) ? post.tags.join(', ') : (post.tags || '');
document.getElementById('editExcerpt').value = post.excerpt || '';
document.getElementById('editContent').value = post.content || '';
// Operator HUD fields
const moodEl = document.getElementById('editMood');
if (moodEl) moodEl.value = post.mood || 'focused';
const hudFields = [
{ id: 'editEnergy', key: 'energy', valId: 'valEnergy' },
{ id: 'editMotivation', key: 'motivation', valId: 'valMotivation' },
{ id: 'editFocus', key: 'focus', valId: 'valFocus' },
{ id: 'editDifficulty', key: 'difficulty', valId: 'valDifficulty' }
];
hudFields.forEach(f => {
const el = document.getElementById(f.id);
const valEl = document.getElementById(f.valId);
if (el) {
el.value = post[f.key] || 3;
if (valEl) valEl.textContent = el.value;
}
});
const coffeeEl = document.getElementById('editCoffee');
if (coffeeEl) coffeeEl.value = post.coffee || 0;
const bpmEl = document.getElementById('editBPM');
if (bpmEl) bpmEl.value = post.bpm || 0;
const threatEl = document.getElementById('editThreat');
if (threatEl) threatEl.value = post.threat_level || 'low';
this.updatePreview();
this.showSection('section-editor');
} catch (err) {
console.error('Edit post error:', err);
this.notify('Failed to load post for editing', 'error');
}
},
async deletePost(slug) {
if (!confirm('Delete post "' + slug + '"? This cannot be undone.')) return;
try {
const res = await fetch(this.API + '/posts/' + encodeURIComponent(slug), {
method: 'DELETE',
headers: this.authHeaders()
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Post deleted');
this.loadPosts();
} catch (err) {
console.error('Delete post error:', err);
this.notify('Failed to delete post', 'error');
}
},
async savePost() {
const postId = document.getElementById('postId').value;
const title = document.getElementById('editTitle').value.trim();
const slug = document.getElementById('editSlug').value.trim();
const date = document.getElementById('editDate').value;
const time = document.getElementById('editTime').value || '00:00';
const tags = document.getElementById('editTags').value;
const excerpt = document.getElementById('editExcerpt').value.trim();
const content = document.getElementById('editContent').value;
if (!title) { this.notify('Title is required', 'error'); return; }
if (!slug) { this.notify('Slug is required', 'error'); return; }
if (!content) { this.notify('Content is required', 'error'); return; }
// Build datetime
const datetime = date ? date + 'T' + time + ':00' : new Date().toISOString();
// Word count
const wordCount = content.trim().split(/\s+/).filter(w => w.length > 0).length;
// Parse tags
const tagList = tags.split(',').map(t => t.trim()).filter(t => t.length > 0);
// Operator HUD
const mood = document.getElementById('editMood')?.value || 'focused';
const energy = parseInt(document.getElementById('editEnergy')?.value || 3);
const motivation = parseInt(document.getElementById('editMotivation')?.value || 3);
const focus = parseInt(document.getElementById('editFocus')?.value || 3);
const difficulty = parseInt(document.getElementById('editDifficulty')?.value || 3);
const coffee = parseInt(document.getElementById('editCoffee')?.value || 0);
const bpm = parseInt(document.getElementById('editBPM')?.value || 0);
const threatLevel = document.getElementById('editThreat')?.value || 'low';
const payload = {
title, slug, date: datetime, tags: tagList, excerpt, content,
word_count: wordCount,
mood, energy, motivation, focus, difficulty, coffee, bpm, threat_level: threatLevel
};
const isEdit = !!postId;
const url = isEdit ? this.API + '/posts/' + encodeURIComponent(postId) : this.API + '/posts';
const method = isEdit ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
throw new Error(errData.error || 'Save failed');
}
this.notify(isEdit ? 'Post updated' : 'Post created');
this.showSection('section-posts');
} catch (err) {
console.error('Save post error:', err);
this.notify('Failed to save: ' + err.message, 'error');
}
},
cancelEdit() {
this.clearEditor();
this.showSection('section-posts');
},
clearEditor() {
document.getElementById('postId').value = '';
document.getElementById('editTitle').value = '';
document.getElementById('editSlug').value = '';
document.getElementById('editDate').value = new Date().toISOString().split('T')[0];
document.getElementById('editTime').value = new Date().toTimeString().slice(0, 5);
document.getElementById('editTags').value = '';
document.getElementById('editExcerpt').value = '';
document.getElementById('editContent').value = '';
const preview = document.getElementById('editorPreview');
if (preview) preview.innerHTML = '';
// Reset HUD
const moodEl = document.getElementById('editMood');
if (moodEl) moodEl.value = 'focused';
['editEnergy', 'editMotivation', 'editFocus', 'editDifficulty'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.value = 3;
const valId = 'val' + id.replace('edit', '');
const valEl = document.getElementById(valId);
if (valEl) valEl.textContent = '3';
}
});
const coffeeEl = document.getElementById('editCoffee');
if (coffeeEl) coffeeEl.value = 0;
const bpmEl = document.getElementById('editBPM');
if (bpmEl) bpmEl.value = 80;
const threatEl = document.getElementById('editThreat');
if (threatEl) threatEl.value = 'low';
const heading = document.getElementById('editorTitle');
if (heading) heading.textContent = 'NEW POST';
},
/* ─────────────────── EDITOR TOOLBAR ─────────────────── */
insertMarkdown(action) {
const textarea = document.getElementById('editContent');
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selected = textarea.value.substring(start, end);
const before = textarea.value.substring(0, start);
const after = textarea.value.substring(end);
let insert = '';
switch (action) {
case 'bold':
insert = `**${selected || 'bold text'}**`;
break;
case 'italic':
insert = `*${selected || 'italic text'}*`;
break;
case 'strikethrough':
insert = `~~${selected || 'strikethrough'}~~`;
break;
case 'code':
insert = '`' + (selected || 'code') + '`';
break;
case 'code-block':
insert = '\n```\n' + (selected || 'code block') + '\n```\n';
break;
case 'h1':
insert = '\n# ' + (selected || 'Heading 1') + '\n';
break;
case 'h2':
insert = '\n## ' + (selected || 'Heading 2') + '\n';
break;
case 'h3':
insert = '\n### ' + (selected || 'Heading 3') + '\n';
break;
case 'link':
insert = selected ? `[${selected}](url)` : '[link text](url)';
break;
case 'image':
insert = selected ? `` : '';
break;
case 'quote':
insert = '\n> ' + (selected || 'blockquote') + '\n';
break;
case 'list-ul':
if (selected) {
insert = '\n' + selected.split('\n').map(l => '- ' + l).join('\n') + '\n';
} else {
insert = '\n- Item 1\n- Item 2\n- Item 3\n';
}
break;
case 'list-ol':
if (selected) {
insert = '\n' + selected.split('\n').map((l, i) => (i + 1) + '. ' + l).join('\n') + '\n';
} else {
insert = '\n1. Item 1\n2. Item 2\n3. Item 3\n';
}
break;
case 'hr':
insert = '\n---\n';
break;
default:
insert = selected;
}
textarea.value = before + insert + after;
textarea.focus();
const newCursor = start + insert.length;
textarea.setSelectionRange(newCursor, newCursor);
this.updatePreview();
},
updatePreview() {
const content = document.getElementById('editContent')?.value || '';
const preview = document.getElementById('editorPreview');
if (!preview) return;
let html = this.escapeHtml(content);
// Code blocks (must process before inline code)
html = html.replace(/```([\s\S]*?)```/g, '$1 ');
// Inline code
html = html.replace(/`([^`]+)`/g, '$1');
// Headers
html = html.replace(/^### (.+)$/gm, '$1 ');
html = html.replace(/^## (.+)$/gm, '$1 ');
html = html.replace(/^# (.+)$/gm, '$1 ');
// Bold and italic
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1 ');
html = html.replace(/\*\*(.+?)\*\*/g, '$1 ');
html = html.replace(/\*(.+?)\*/g, '$1 ');
// Strikethrough
html = html.replace(/~~(.+?)~~/g, '$1');
// Blockquotes
html = html.replace(/^> (.+)$/gm, '$1 ');
// Horizontal rule
html = html.replace(/^---$/gm, ' ');
// Images
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, ' ');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ');
// Unordered lists
html = html.replace(/^- (.+)$/gm, '$1 ');
html = html.replace(/(.*<\/li>\n?)+/g, '');
// Ordered lists
html = html.replace(/^\d+\. (.+)$/gm, ' $1 ');
// Paragraphs: double newline
html = html.replace(/\n\n/g, '');
html = '
' + html + '
';
// Single newlines to br (within paragraphs)
html = html.replace(/\n/g, ' ');
// Clean empty paragraphs
html = html.replace(/<\/p>/g, '');
preview.innerHTML = html;
},
autoSlug() {
const title = document.getElementById('editTitle')?.value || '';
const slug = title
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const slugEl = document.getElementById('editSlug');
if (slugEl) slugEl.value = slug;
},
/* ─────────────────── TRACKS ─────────────────── */
async loadTracks() {
try {
const res = await fetch(this.API + '/tracks', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load tracks');
const data = await res.json();
const tracks = Array.isArray(data) ? data : (data.tracks || []);
const container = document.getElementById('tracksList');
if (!container) return;
if (tracks.length === 0) {
container.innerHTML = '
No tracks in library
';
return;
}
container.innerHTML = tracks.map((t, i) => {
const coverHtml = t.cover
? ` `
: '♪
';
return `
${coverHtml}
${this.escapeHtml(t.artist || 'Unknown')} — ${this.escapeHtml(t.title || 'Untitled')}
${this.escapeHtml(t.album || '')} ${t.genre ? '· ' + this.escapeHtml(t.genre) : ''}
${t.url ? `
▶ ` : ''}
✗
`;
}).join('');
} catch (err) {
console.error('Tracks load error:', err);
this.notify('Failed to load tracks', 'error');
}
},
async addTrack() {
const title = document.getElementById('trackTitle')?.value.trim();
const artist = document.getElementById('trackArtist')?.value.trim();
const album = document.getElementById('trackAlbum')?.value.trim();
const genre = document.getElementById('trackGenre')?.value.trim();
const url = document.getElementById('trackUrl')?.value.trim();
const cover = document.getElementById('trackCover')?.value.trim();
if (!title || !artist) {
this.notify('Title and artist are required', 'error');
return;
}
const payload = { title, artist, album, genre, url, cover };
try {
const res = await fetch(this.API + '/tracks', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Failed to add track');
this.notify('Track added');
// Clear inputs
['trackTitle', 'trackArtist', 'trackAlbum', 'trackGenre', 'trackUrl', 'trackCover'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
this.loadTracks();
} catch (err) {
console.error('Add track error:', err);
this.notify('Failed to add track', 'error');
}
},
async deleteTrack(index) {
if (!confirm('Delete this track?')) return;
try {
const res = await fetch(this.API + '/tracks/' + index, {
method: 'DELETE',
headers: this.authHeaders()
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Track deleted');
this.loadTracks();
} catch (err) {
console.error('Delete track error:', err);
this.notify('Failed to delete track', 'error');
}
},
/* ─────────────────── SETTINGS (WIDGET TOGGLES) ─────────────────── */
async loadSettings() {
try {
const res = await fetch(this.API + '/settings', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load settings');
const settings = await res.json();
// Set toggle states
document.querySelectorAll('[data-key]').forEach(toggle => {
const key = toggle.getAttribute('data-key');
if (settings.hasOwnProperty(key)) {
toggle.checked = !!settings[key];
}
});
} catch (err) {
console.error('Settings load error:', err);
this.notify('Failed to load settings', 'error');
}
},
async saveSettings() {
const settings = {};
document.querySelectorAll('[data-key]').forEach(toggle => {
settings[toggle.getAttribute('data-key')] = toggle.checked;
});
try {
const res = await fetch(this.API + '/settings', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(settings)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Settings saved');
} catch (err) {
console.error('Save settings error:', err);
this.notify('Failed to save settings', 'error');
}
},
/* ─────────────────── HOMEPAGE ─────────────────── */
async loadHomepage() {
try {
const res = await fetch(this.API + '/homepage', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load homepage config');
const data = await res.json();
this.homepageData = data;
document.getElementById('heroTitle').value = data.hero_title || '';
document.getElementById('heroSubtitle').value = data.hero_subtitle || '';
document.getElementById('heroTagline').value = data.hero_tagline || '';
document.getElementById('aboutText').value = data.about_text || '';
this.renderHomepageSections(data.sections || []);
} catch (err) {
console.error('Homepage load error:', err);
this.notify('Failed to load homepage config', 'error');
}
},
renderHomepageSections(sections) {
const container = document.getElementById('homepageSections');
if (!container) return;
const defaultSections = [
{ key: 'about', label: 'About', visible: true },
{ key: 'blog', label: 'Blog', visible: true },
{ key: 'dev-cards', label: 'Dev Cards', visible: true },
{ key: 'links', label: 'Links', visible: true },
{ key: 'contact', label: 'Contact', visible: true },
{ key: 'terminal', label: 'Terminal', visible: true }
];
const sectionList = sections.length > 0 ? sections : defaultSections;
if (this.homepageData) this.homepageData.sections = sectionList;
container.innerHTML = sectionList.map((s, i) => {
const key = s.key || s.name || s;
const label = s.label || s.name || key;
const visible = s.visible !== false;
return `
${this.escapeHtml(label)}
▲
▼
`;
}).join('');
},
toggleHomepageSection(key, visible) {
if (!this.homepageData || !this.homepageData.sections) return;
const section = this.homepageData.sections.find(s => (s.key || s.name || s) === key);
if (section) section.visible = visible;
},
moveSection(sectionKey, direction) {
if (!this.homepageData || !this.homepageData.sections) return;
const sections = this.homepageData.sections;
const idx = sections.findIndex(s => (s.key || s.name || s) === sectionKey);
if (idx === -1) return;
const targetIdx = direction === 'up' ? idx - 1 : idx + 1;
if (targetIdx < 0 || targetIdx >= sections.length) return;
// Swap
[sections[idx], sections[targetIdx]] = [sections[targetIdx], sections[idx]];
this.renderHomepageSections(sections);
},
async saveHomepage() {
const payload = {
hero_title: document.getElementById('heroTitle')?.value.trim() || '',
hero_subtitle: document.getElementById('heroSubtitle')?.value.trim() || '',
hero_tagline: document.getElementById('heroTagline')?.value.trim() || '',
about_text: document.getElementById('aboutText')?.value.trim() || '',
sections: this.homepageData?.sections || []
};
try {
const res = await fetch(this.API + '/homepage', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Homepage config saved');
} catch (err) {
console.error('Save homepage error:', err);
this.notify('Failed to save homepage config', 'error');
}
},
/* ─────────────────── SERVICES ─────────────────── */
async loadServices() {
try {
const res = await fetch(this.API + '/services/managed', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load services');
const data = await res.json();
this.servicesData = Array.isArray(data) ? data : (data.services || []);
const container = document.getElementById('managedServicesList');
if (!container) return;
if (this.servicesData.length === 0) {
container.innerHTML = 'No managed services
';
return;
}
container.innerHTML = this.servicesData.map((s, i) => {
return `
${this.escapeHtml(s.name || 'Unnamed')}
${this.escapeHtml(s.url || '')}
✗
`;
}).join('');
} catch (err) {
console.error('Services load error:', err);
this.notify('Failed to load services', 'error');
}
},
async addService() {
const name = document.getElementById('serviceName')?.value.trim();
const url = document.getElementById('serviceUrl')?.value.trim();
if (!name) {
this.notify('Service name is required', 'error');
return;
}
this.servicesData.push({ name, url });
try {
const res = await fetch(this.API + '/services/managed', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.servicesData)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Service added');
document.getElementById('serviceName').value = '';
document.getElementById('serviceUrl').value = '';
this.loadServices();
} catch (err) {
console.error('Add service error:', err);
this.servicesData.pop();
this.notify('Failed to add service', 'error');
}
},
async deleteService(index) {
if (!confirm('Delete this service?')) return;
this.servicesData.splice(index, 1);
try {
const res = await fetch(this.API + '/services/managed', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.servicesData)
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Service deleted');
this.loadServices();
} catch (err) {
console.error('Delete service error:', err);
this.notify('Failed to delete service', 'error');
}
},
/* ─────────────────── NAVIGATION EDITOR ─────────────────── */
async loadNavigation() {
try {
const res = await fetch(this.API + '/navigation', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load navigation');
const data = await res.json();
this.navData = Array.isArray(data) ? data : (data.items || []);
const container = document.getElementById('navItemsList');
if (!container) return;
if (this.navData.length === 0) {
container.innerHTML = 'No navigation items
';
return;
}
// Sort by order
this.navData.sort((a, b) => (a.order || 0) - (b.order || 0));
container.innerHTML = this.navData.map((item, i) => {
return `
#${item.order || i}
${this.escapeHtml(item.label || 'Unnamed')}
${this.escapeHtml(item.url || '')}
✗
`;
}).join('');
} catch (err) {
console.error('Navigation load error:', err);
this.notify('Failed to load navigation', 'error');
}
},
async addNavItem() {
const label = document.getElementById('navLabel')?.value.trim();
const url = document.getElementById('navUrl')?.value.trim();
const order = parseInt(document.getElementById('navOrder')?.value || 0);
if (!label || !url) {
this.notify('Label and URL are required', 'error');
return;
}
this.navData.push({ label, url, order });
try {
const res = await fetch(this.API + '/navigation', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.navData)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Nav item added');
document.getElementById('navLabel').value = '';
document.getElementById('navUrl').value = '';
document.getElementById('navOrder').value = '';
this.loadNavigation();
} catch (err) {
console.error('Add nav item error:', err);
this.navData.pop();
this.notify('Failed to add nav item', 'error');
}
},
async deleteNavItem(index) {
if (!confirm('Delete this navigation item?')) return;
this.navData.splice(index, 1);
try {
const res = await fetch(this.API + '/navigation', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.navData)
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Nav item deleted');
this.loadNavigation();
} catch (err) {
console.error('Delete nav item error:', err);
this.notify('Failed to delete nav item', 'error');
}
},
/* ─────────────────── LINKS ─────────────────── */
async loadLinks() {
try {
const res = await fetch(this.API + '/links', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load links');
const data = await res.json();
this.linksData = Array.isArray(data) ? data : (data.links || []);
const container = document.getElementById('managedLinksList');
if (!container) return;
if (this.linksData.length === 0) {
container.innerHTML = 'No links configured
';
return;
}
container.innerHTML = this.linksData.map((link, i) => {
return `
${this.escapeHtml(link.icon || '🔗')}
${this.escapeHtml(link.name || 'Unnamed')}
${this.escapeHtml(link.url || '')} ${link.category ? '· ' + this.escapeHtml(link.category) + ' ' : ''}
✗
`;
}).join('');
} catch (err) {
console.error('Links load error:', err);
this.notify('Failed to load links', 'error');
}
},
async addLink() {
const name = document.getElementById('linkName')?.value.trim();
const url = document.getElementById('linkUrl')?.value.trim();
const icon = document.getElementById('linkIcon')?.value.trim();
const category = document.getElementById('linkCategory')?.value.trim();
if (!name || !url) {
this.notify('Name and URL are required', 'error');
return;
}
this.linksData.push({ name, url, icon: icon || '🔗', category: category || 'general' });
try {
const res = await fetch(this.API + '/links', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.linksData)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Link added');
['linkName', 'linkUrl', 'linkIcon', 'linkCategory'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
this.loadLinks();
} catch (err) {
console.error('Add link error:', err);
this.linksData.pop();
this.notify('Failed to add link', 'error');
}
},
async deleteLink(index) {
if (!confirm('Delete this link?')) return;
this.linksData.splice(index, 1);
try {
const res = await fetch(this.API + '/links', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(this.linksData)
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Link deleted');
this.loadLinks();
} catch (err) {
console.error('Delete link error:', err);
this.notify('Failed to delete link', 'error');
}
},
/* ─────────────────── API KEYS ─────────────────── */
async loadApiKeys() {
try {
const res = await fetch(this.API + '/apikeys', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load API keys');
const data = await res.json();
// Weather
this.setVal('weatherApiKey', data.weather_api_key);
// Spotify
this.setVal('spotifyClientId', data.spotify_client_id);
this.setVal('spotifyClientSecret', data.spotify_client_secret);
this.setVal('spotifyRefreshToken', data.spotify_refresh_token);
// SMTP
this.setVal('smtpHost', data.smtp_host);
this.setVal('smtpPort', data.smtp_port);
this.setVal('smtpUser', data.smtp_user);
this.setVal('smtpPass', data.smtp_pass);
// Discord / GitHub
this.setVal('discordWebhook', data.discord_webhook);
this.setVal('githubToken', data.github_token);
// Custom APIs
for (let i = 1; i <= 3; i++) {
this.setVal(`customApi${i}Name`, data[`custom_api_${i}_name`]);
this.setVal(`customApi${i}Key`, data[`custom_api_${i}_key`]);
this.setVal(`customApi${i}Url`, data[`custom_api_${i}_url`]);
}
} catch (err) {
console.error('API keys load error:', err);
this.notify('Failed to load API keys', 'error');
}
},
async saveApiKey(group) {
const payload = { group };
switch (group) {
case 'weather':
payload.weather_api_key = this.getVal('weatherApiKey');
break;
case 'spotify':
payload.spotify_client_id = this.getVal('spotifyClientId');
payload.spotify_client_secret = this.getVal('spotifyClientSecret');
payload.spotify_refresh_token = this.getVal('spotifyRefreshToken');
break;
case 'smtp':
payload.smtp_host = this.getVal('smtpHost');
payload.smtp_port = this.getVal('smtpPort');
payload.smtp_user = this.getVal('smtpUser');
payload.smtp_pass = this.getVal('smtpPass');
break;
case 'discord':
payload.discord_webhook = this.getVal('discordWebhook');
break;
case 'github':
payload.github_token = this.getVal('githubToken');
break;
case 'custom1':
payload.custom_api_1_name = this.getVal('customApi1Name');
payload.custom_api_1_key = this.getVal('customApi1Key');
payload.custom_api_1_url = this.getVal('customApi1Url');
break;
case 'custom2':
payload.custom_api_2_name = this.getVal('customApi2Name');
payload.custom_api_2_key = this.getVal('customApi2Key');
payload.custom_api_2_url = this.getVal('customApi2Url');
break;
case 'custom3':
payload.custom_api_3_name = this.getVal('customApi3Name');
payload.custom_api_3_key = this.getVal('customApi3Key');
payload.custom_api_3_url = this.getVal('customApi3Url');
break;
default:
this.notify('Unknown API key group: ' + group, 'error');
return;
}
try {
const res = await fetch(this.API + '/apikeys', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('API keys saved (' + group + ')');
} catch (err) {
console.error('Save API key error:', err);
this.notify('Failed to save API keys', 'error');
}
},
/* ─────────────────── THEME ─────────────────── */
async loadTheme() {
try {
const res = await fetch(this.API + '/theme', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load theme');
const data = await res.json();
// Colours
this.setVal('themeAccent', data.accent || this.themeDefaults.accent);
this.setVal('themeAccentHex', data.accent || this.themeDefaults.accent);
this.setVal('themeBg', data.bg || this.themeDefaults.bg);
this.setVal('themeBgHex', data.bg || this.themeDefaults.bg);
this.setVal('themeText', data.text || this.themeDefaults.text);
this.setVal('themeTextHex', data.text || this.themeDefaults.text);
// Effect toggles
this.setChecked('themeScanlines', data.scanlines !== undefined ? data.scanlines : this.themeDefaults.scanlines);
this.setChecked('themeParticles', data.particles !== undefined ? data.particles : this.themeDefaults.particles);
this.setChecked('themeGlitch', data.glitch !== undefined ? data.glitch : this.themeDefaults.glitch);
this.setChecked('themeGrid', data.grid !== undefined ? data.grid : this.themeDefaults.grid);
// Font size
const fontSize = data.font_size || this.themeDefaults.fontSize;
this.setVal('themeFontSize', fontSize);
const valFont = document.getElementById('valFontSize');
if (valFont) valFont.textContent = fontSize;
} catch (err) {
console.error('Theme load error:', err);
this.notify('Failed to load theme', 'error');
}
},
async saveTheme() {
const payload = {
accent: this.getVal('themeAccentHex') || this.getVal('themeAccent'),
bg: this.getVal('themeBgHex') || this.getVal('themeBg'),
text: this.getVal('themeTextHex') || this.getVal('themeText'),
scanlines: this.getChecked('themeScanlines'),
particles: this.getChecked('themeParticles'),
glitch: this.getChecked('themeGlitch'),
grid: this.getChecked('themeGrid'),
font_size: parseInt(this.getVal('themeFontSize') || 16)
};
try {
const res = await fetch(this.API + '/theme', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Theme saved');
} catch (err) {
console.error('Save theme error:', err);
this.notify('Failed to save theme', 'error');
}
},
resetTheme() {
if (!confirm('Reset theme to defaults?')) return;
this.setVal('themeAccent', this.themeDefaults.accent);
this.setVal('themeAccentHex', this.themeDefaults.accent);
this.setVal('themeBg', this.themeDefaults.bg);
this.setVal('themeBgHex', this.themeDefaults.bg);
this.setVal('themeText', this.themeDefaults.text);
this.setVal('themeTextHex', this.themeDefaults.text);
this.setChecked('themeScanlines', this.themeDefaults.scanlines);
this.setChecked('themeParticles', this.themeDefaults.particles);
this.setChecked('themeGlitch', this.themeDefaults.glitch);
this.setChecked('themeGrid', this.themeDefaults.grid);
this.setVal('themeFontSize', this.themeDefaults.fontSize);
const valFont = document.getElementById('valFontSize');
if (valFont) valFont.textContent = this.themeDefaults.fontSize;
this.notify('Theme reset to defaults — save to apply', 'info');
},
syncColorInput(pickerId, hexId) {
const picker = document.getElementById(pickerId);
const hex = document.getElementById(hexId);
if (picker && hex) hex.value = picker.value;
},
/* ─────────────────── SEO ─────────────────── */
async loadSeo() {
try {
const res = await fetch(this.API + '/seo', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load SEO settings');
const data = await res.json();
this.setVal('seoTitle', data.title);
this.setVal('seoDescription', data.description);
this.setVal('seoKeywords', data.keywords);
this.setVal('seoOgImage', data.og_image);
this.setVal('seoFavicon', data.favicon);
} catch (err) {
console.error('SEO load error:', err);
this.notify('Failed to load SEO settings', 'error');
}
},
async saveSeo() {
const payload = {
title: this.getVal('seoTitle'),
description: this.getVal('seoDescription'),
keywords: this.getVal('seoKeywords'),
og_image: this.getVal('seoOgImage'),
favicon: this.getVal('seoFavicon')
};
try {
const res = await fetch(this.API + '/seo', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('SEO settings saved');
} catch (err) {
console.error('Save SEO error:', err);
this.notify('Failed to save SEO settings', 'error');
}
},
/* ─────────────────── CONTACT ─────────────────── */
async loadContactSettings() {
try {
const res = await fetch(this.API + '/contact-settings', { headers: this.authHeaders() });
if (!res.ok) throw new Error('Failed to load contact settings');
const data = await res.json();
this.setVal('contactEmail', data.email);
this.setVal('contactAutoReply', data.auto_reply);
this.setChecked('contactFormEnabled', data.form_enabled !== false);
} catch (err) {
console.error('Contact settings load error:', err);
this.notify('Failed to load contact settings', 'error');
}
},
async saveContactSettings() {
const payload = {
email: this.getVal('contactEmail'),
auto_reply: this.getVal('contactAutoReply'),
form_enabled: this.getChecked('contactFormEnabled')
};
try {
const res = await fetch(this.API + '/contact-settings', {
method: 'POST',
headers: this.authHeaders(),
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
this.notify('Contact settings saved');
} catch (err) {
console.error('Save contact settings error:', err);
this.notify('Failed to save contact settings', 'error');
}
},
/* ─────────────────── BACKUPS ─────────────────── */
async downloadBackup(type) {
try {
const url = this.API + '/backups/' + type;
const res = await fetch(url, { headers: this.authHeaders() });
if (!res.ok) throw new Error('Download failed');
const blob = await res.blob();
const filename = type === 'all' ? 'jaeswift-backup-all.zip' : type + '.json';
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
this.notify('Backup downloaded: ' + filename);
} catch (err) {
console.error('Backup download error:', err);
this.notify('Failed to download backup', 'error');
}
},
/* ─────────────────── UTILITIES ─────────────────── */
escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
},
setVal(id, value) {
const el = document.getElementById(id);
if (el) el.value = value || '';
},
getVal(id) {
const el = document.getElementById(id);
return el ? el.value.trim() : '';
},
setChecked(id, checked) {
const el = document.getElementById(id);
if (el) el.checked = !!checked;
},
getChecked(id) {
const el = document.getElementById(id);
return el ? el.checked : false;
}
};
/* ─────────────────── BOOTSTRAP ─────────────────── */
document.addEventListener('DOMContentLoaded', () => AdminApp.init());