/* ═══════════════════════════════════════════════════════════════ * 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: '#00ff41', bg: '#111111', text: '#e0e0e0', scanlines: true, particles: true, glitch: false, grid: true, fontSize: 16 }, /* ───────────────────────── AUTH ───────────────────────── */ init() { // Login form - support both
submit and button click const loginForm = document.getElementById('loginForm'); if (loginForm) { loginForm.addEventListener('submit', (e) => this.login(e)); } const loginBtn = document.getElementById('loginBtn'); if (loginBtn && !loginForm) { loginBtn.addEventListener('click', (e) => this.login(e)); } // Also handle Enter key in password field const loginPass = document.getElementById('loginPassword'); if (loginPass) { loginPass.addEventListener('keydown', (e) => { if (e.key === 'Enter') 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('editorTitle'); if (editTitle) { editTitle.addEventListener('input', () => this.autoSlug()); } // Editor: live preview on content input const editContent = document.getElementById('editorContent'); 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 ['hudEnergy', 'hudMotivation', 'hudFocus', 'hudDifficulty'].forEach(id => { const slider = document.getElementById(id); if (slider) { const valId = 'val' + id.replace('hud', ''); const valEl = document.getElementById(valId); if (valEl) valEl.textContent = slider.value; slider.addEventListener('input', () => { if (valEl) valEl.textContent = slider.value; }); } }); // Theme colour sync ['AccentColor', 'BgColor', 'TextColor'].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('themeFontSizeVal'); if (fontSlider && valFont) { valFont.textContent = fontSlider.value; fontSlider.addEventListener('input', () => { valFont.textContent = fontSlider.value; }); } }, async login(e) { if (e && e.preventDefault) e.preventDefault(); const user = document.getElementById('loginUsername').value.trim(); const pass = document.getElementById('loginPassword').value; const errEl = document.getElementById('loginError'); if (errEl) errEl.textContent = ''; try { const res = await fetch(this.API + '/auth/login', { 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) { // Normalize: ensure name has section- prefix if (!name.startsWith('section-')) name = 'section-' + 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 shortName = name.replace('section-', ''); const activeLink = document.querySelector(`.sidebar-link[data-section="${shortName}"]`); 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('notification') || 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 = '#00ff41'; } 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() { const results = await Promise.allSettled([ 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 try { if (results[0].status === 'fulfilled' && results[0].value.ok) { const stats = await results[0].value.json(); const cpu = document.getElementById('statCPU'); const mem = document.getElementById('statMEM'); const disk = document.getElementById('statDISK'); 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) + '%'; } } catch (e) { console.warn('Dashboard stats error:', e); } // Posts count + word count try { if (results[1].status === 'fulfilled' && results[1].value.ok) { const posts = await results[1].value.json(); const arr = Array.isArray(posts) ? posts : (posts.posts || []); const dashPosts = document.getElementById('statPosts'); const dashWords = document.getElementById('statWords'); if (dashPosts) dashPosts.textContent = arr.length; if (dashWords) { const totalWords = arr.reduce((sum, p) => sum + (p.word_count || 0), 0); dashWords.textContent = totalWords.toLocaleString(); } } } catch (e) { console.warn('Dashboard posts error:', e); } // Tracks count try { if (results[2].status === 'fulfilled' && results[2].value.ok) { const tracks = await results[2].value.json(); const arr = Array.isArray(tracks) ? tracks : (tracks.tracks || []); const dashTracks = document.getElementById('statTracks'); if (dashTracks) dashTracks.textContent = arr.length; } } catch (e) { console.warn('Dashboard tracks error:', e); } // Services grid try { if (results[3].status === 'fulfilled' && results[3].value.ok) { const services = await results[3].value.json(); const arr = Array.isArray(services) ? services : (services.services || []); const grid = document.getElementById('servicesGrid'); if (grid) { grid.innerHTML = arr.map(svc => { const isUp = svc.status === 'up' || svc.status === 'online' || svc.status === true; const dotColor = isUp ? '#00ff41' : '#ff3232'; const statusText = isUp ? 'ONLINE' : 'OFFLINE'; return `
${this.escapeHtml(svc.name || svc.service || 'Unknown')} ${statusText}
`; }).join(''); } } else { const grid = document.getElementById('servicesGrid'); if (grid) grid.innerHTML = '
Services check timed out
'; } } catch (e) { console.warn('Dashboard services error:', e); } // Threats try { if (results[4].status === 'fulfilled' && results[4].value.ok) { const threats = await results[4].value.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 \u2713
'; } else { container.innerHTML = arr.map(t => { const severity = (t.severity || t.level || 'low').toLowerCase(); const sevColor = severity === 'critical' ? '#ff0040' : severity === 'high' ? '#ff6600' : severity === 'medium' ? '#c9a227' : '#00ff41'; return `
${this.escapeHtml(t.cve || t.id || t.name || 'Unknown')} ${severity}
${this.escapeHtml(t.description || t.summary || '')}
`; }).join(''); } } } } catch (e) { console.warn('Dashboard threats error:', e); } }, /* ─────────────────── 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('editorPostId').value = post.slug || slug; document.getElementById('editorTitle').value = post.title || ''; document.getElementById('editorSlug').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('editorDate').value = dateStr; document.getElementById('editorTime').value = timeStr; } else { document.getElementById('editorDate').value = ''; document.getElementById('editorTime').value = ''; } document.getElementById('editorTags').value = Array.isArray(post.tags) ? post.tags.join(', ') : (post.tags || ''); document.getElementById('editorExcerpt').value = post.excerpt || ''; document.getElementById('editorContent').value = post.content || ''; // Operator HUD fields const moodEl = document.getElementById('hudMood'); if (moodEl) moodEl.value = post.mood || 'focused'; const hudFields = [ { id: 'hudEnergy', key: 'energy', valId: 'valEnergy' }, { id: 'hudMotivation', key: 'motivation', valId: 'valMotivation' }, { id: 'hudFocus', key: 'focus', valId: 'valFocus' }, { id: 'hudDifficulty', 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('hudCoffee'); if (coffeeEl) coffeeEl.value = post.coffee || 0; const bpmEl = document.getElementById('hudBPM'); if (bpmEl) bpmEl.value = post.bpm || 0; const threatEl = document.getElementById('hudThreat'); 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('editorPostId').value; const title = document.getElementById('editorTitle').value.trim(); const slug = document.getElementById('editorSlug').value.trim(); const date = document.getElementById('editorDate').value; const time = document.getElementById('editorTime').value || '00:00'; const tags = document.getElementById('editorTags').value; const excerpt = document.getElementById('editorExcerpt').value.trim(); const content = document.getElementById('editorContent').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('hudMood')?.value || 'focused'; const energy = parseInt(document.getElementById('hudEnergy')?.value || 3); const motivation = parseInt(document.getElementById('hudMotivation')?.value || 3); const focus = parseInt(document.getElementById('hudFocus')?.value || 3); const difficulty = parseInt(document.getElementById('hudDifficulty')?.value || 3); const coffee = parseInt(document.getElementById('hudCoffee')?.value || 0); const bpm = parseInt(document.getElementById('hudBPM')?.value || 0); const threatLevel = document.getElementById('hudThreat')?.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('editorPostId').value = ''; document.getElementById('editorTitle').value = ''; document.getElementById('editorSlug').value = ''; document.getElementById('editorDate').value = new Date().toISOString().split('T')[0]; document.getElementById('editorTime').value = new Date().toTimeString().slice(0, 5); document.getElementById('editorTags').value = ''; document.getElementById('editorExcerpt').value = ''; document.getElementById('editorContent').value = ''; const preview = document.getElementById('editorPreview'); if (preview) preview.innerHTML = ''; // Reset HUD const moodEl = document.getElementById('hudMood'); if (moodEl) moodEl.value = 'focused'; ['hudEnergy', 'hudMotivation', 'hudFocus', 'hudDifficulty'].forEach(id => { const el = document.getElementById(id); if (el) { el.value = 3; const valId = 'val' + id.replace('hud', ''); const valEl = document.getElementById(valId); if (valEl) valEl.textContent = '3'; } }); const coffeeEl = document.getElementById('hudCoffee'); if (coffeeEl) coffeeEl.value = 0; const bpmEl = document.getElementById('hudBPM'); if (bpmEl) bpmEl.value = 80; const threatEl = document.getElementById('hudThreat'); if (threatEl) threatEl.value = 'low'; const heading = document.getElementById('editorTitle'); if (heading) heading.textContent = 'NEW POST'; }, /* ─────────────────── EDITOR TOOLBAR ─────────────────── */ insertMarkdown(action) { const textarea = document.getElementById('editorContent'); 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 ? `![${selected}](url)` : '![alt text](image-url)'; 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('editorContent')?.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, '$1'); // 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('editorTitle')?.value || ''; const slug = title .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); const slugEl = document.getElementById('editorSlug'); 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 ? `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('servicesList'); 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({ name, url }) }); 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({ name, url }) }); 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('navList'); 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 ``; }).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({ label, url, order }) }); 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; try { const res = await fetch(this.API + '/navigation/' + index, { method: 'DELETE', headers: this.authHeaders() }); 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('linksList'); if (!container) return; if (this.linksData.length === 0) { container.innerHTML = '
    No links configured
    '; return; } container.innerHTML = this.linksData.map((link, i) => { return ``; }).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({ name, url, icon: icon || '🔗', category: category || 'general' }) }); 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({ name, url, icon: icon || '🔗', category: category || 'general' }) }); 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('apiWeatherKey', data.weather_api_key); // Spotify this.setVal('apiSpotifyClientId', data.spotify_client_id); this.setVal('apiSpotifyClientSecret', data.spotify_client_secret); this.setVal('apiSpotifyRefreshToken', data.spotify_refresh_token); // SMTP this.setVal('apiSmtpHost', data.smtp_host); this.setVal('apiSmtpPort', data.smtp_port); this.setVal('apiSmtpUser', data.smtp_user); this.setVal('apiSmtpPass', data.smtp_pass); // Discord / GitHub this.setVal('apiDiscordWebhook', data.discord_webhook); this.setVal('apiGithubToken', data.github_token); // Custom APIs for (let i = 1; i <= 3; i++) { this.setVal(`apiCustom${i}Name`, data[`custom_api_${i}_name`]); this.setVal(`apiCustom${i}Key`, data[`custom_api_${i}_key`]); this.setVal(`apiCustom${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 data = {}; switch (group) { case 'weather': data.api_key = this.getVal('apiWeatherKey'); break; case 'spotify': data.client_id = this.getVal('apiSpotifyClientId'); data.client_secret = this.getVal('apiSpotifyClientSecret'); data.refresh_token = this.getVal('apiSpotifyRefreshToken'); break; case 'smtp': data.host = this.getVal('apiSmtpHost'); data.port = this.getVal('apiSmtpPort'); data.user = this.getVal('apiSmtpUser'); data.pass = this.getVal('apiSmtpPass'); break; case 'discord': data.webhook = this.getVal('apiDiscordWebhook'); break; case 'github': data.token = this.getVal('apiGithubToken'); break; case 'custom1': data.name = this.getVal('apiCustom1Name'); data.key = this.getVal('apiCustom1Key'); data.url = this.getVal('apiCustom1URL'); break; case 'custom2': data.name = this.getVal('apiCustom2Name'); data.key = this.getVal('apiCustom2Key'); data.url = this.getVal('apiCustom2URL'); break; case 'custom3': data.name = this.getVal('apiCustom3Name'); data.key = this.getVal('apiCustom3Key'); data.url = this.getVal('apiCustom3URL'); 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({ group, data }) }); 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('themeAccentColor', data.accent || this.themeDefaults.accent); this.setVal('themeAccentColorHex', data.accent || this.themeDefaults.accent); this.setVal('themeBgColor', data.bg || this.themeDefaults.bg); this.setVal('themeBgColorHex', data.bg || this.themeDefaults.bg); this.setVal('themeTextColor', data.text || this.themeDefaults.text); this.setVal('themeTextColorHex', 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('themeGridBg', 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('themeFontSizeVal'); 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('themeAccentColorHex') || this.getVal('themeAccentColor'), bg: this.getVal('themeBgColorHex') || this.getVal('themeBgColor'), text: this.getVal('themeTextColorHex') || this.getVal('themeTextColor'), scanlines: this.getChecked('themeScanlines'), particles: this.getChecked('themeParticles'), glitch: this.getChecked('themeGlitch'), grid: this.getChecked('themeGridBg'), 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('themeAccentColor', this.themeDefaults.accent); this.setVal('themeAccentColorHex', this.themeDefaults.accent); this.setVal('themeBgColor', this.themeDefaults.bg); this.setVal('themeBgColorHex', this.themeDefaults.bg); this.setVal('themeTextColor', this.themeDefaults.text); this.setVal('themeTextColorHex', this.themeDefaults.text); this.setChecked('themeScanlines', this.themeDefaults.scanlines); this.setChecked('themeParticles', this.themeDefaults.particles); this.setChecked('themeGlitch', this.themeDefaults.glitch); this.setChecked('themeGridBg', this.themeDefaults.grid); this.setVal('themeFontSize', this.themeDefaults.fontSize); const valFont = document.getElementById('themeFontSizeVal'); 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());