diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md
new file mode 100644
index 0000000..4f492c7
--- /dev/null
+++ b/AUDIT_REPORT.md
@@ -0,0 +1,658 @@
+# JAESWIFT.XYZ — FULL SITE AUDIT REPORT
+**Date:** 2026-04-01
+**Auditor:** Agent Zero Master Developer
+**Scope:** All HTML, CSS, JS, Flask API, nginx config, live endpoint testing
+
+---
+
+## EXECUTIVE SUMMARY
+
+The site's core infrastructure works: nginx serves pages (200 OK), Flask API runs on port 5000, all 9 blog post slugs resolve correctly, and static assets load. However, the **admin panel is critically broken** due to a systematic naming mismatch between `admin.js` and `admin.html` — 60 DOM element IDs referenced in JavaScript don't exist in the HTML. Additionally, 5 onclick handlers call undefined methods, 51 CSS classes are used in HTML but never defined, and several API payload shapes don't match between frontend and backend.
+
+### Issue Count by Severity
+| Severity | Count |
+|----------|-------|
+| CRITICAL | 8 |
+| HIGH | 12 |
+| MEDIUM | 15 |
+| LOW | 7 |
+
+---
+
+## 1. CRITICAL ISSUES
+
+### 1.1 ❌ CRITICAL: 60 DOM ID Mismatches — admin.js ↔ admin.html
+**Impact:** The entire admin panel is non-functional. Every section that reads/writes form fields silently fails because `getElementById()` returns `null`.
+
+#### 1.1.1 Editor Section (16 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `postId` | `editorPostId` | admin.js:451,524 |
+| `editTitle` | `editorTitle` | admin.js:66,452,525 |
+| `editSlug` | `editorSlug` | admin.js:453,526 |
+| `editDate` | `editorDate` | admin.js:460,463,527 |
+| `editTime` | `editorTime` | admin.js:461,464,528 |
+| `editTags` | `editorTags` | admin.js:467 |
+| `editExcerpt` | `editorExcerpt` | admin.js:468 |
+| `editContent` | `editorContent` | admin.js:72,469 |
+| `editMood` | `hudMood` | admin.js:472 |
+| `editEnergy` | `hudEnergy` | admin.js:85,476 |
+| `editMotivation` | `hudMotivation` | admin.js:85,477 |
+| `editFocus` | `hudFocus` | admin.js:85,478 |
+| `editDifficulty` | `hudDifficulty` | admin.js:85,479 |
+| `editCoffee` | `hudCoffee` | admin.js:490 |
+| `editBPM` | `hudBPM` | admin.js:493 |
+| `editThreat` | `hudThreat` | admin.js:496 |
+
+**Fix:** In `admin.js`, rename all `editX` → `editorX` and `editMood/Energy/etc` → `hudMood/Energy/etc`, `postId` → `editorPostId`.
+
+#### 1.1.2 Dashboard Section (8 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `dashCPU` | `statCPU` | admin.js:318 |
+| `dashMEM` | `statMEM` | admin.js:319 |
+| `dashDISK` | `statDISK` | admin.js:320 |
+| `dashPosts` | `statPosts` | admin.js:330 |
+| `dashWords` | `statWords` | admin.js:331 |
+| `dashTracks` | `statTracks` | admin.js:343 |
+| `dashServicesGrid` | `servicesGrid` | admin.js:351 |
+| `dashThreats` | *(not in HTML)* | admin.js:370 |
+
+**Fix:** In `admin.js`, rename `dashX` → `statX`, `dashServicesGrid` → `servicesGrid`. Add a threats container `
` to the dashboard section of `admin.html`, OR rename the JS reference.
+
+#### 1.1.3 API Keys Section (19 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `weatherApiKey` | `apiWeatherKey` | admin.js:1294 |
+| `spotifyClientId` | `apiSpotifyClientId` | admin.js:1297 |
+| `spotifyClientSecret` | `apiSpotifyClientSecret` | admin.js:1298 |
+| `spotifyRefreshToken` | `apiSpotifyRefreshToken` | admin.js:1299 |
+| `smtpHost` | `apiSmtpHost` | admin.js:1302 |
+| `smtpPort` | `apiSmtpPort` | admin.js:1303 |
+| `smtpUser` | `apiSmtpUser` | admin.js:1304 |
+| `smtpPass` | `apiSmtpPass` | admin.js:1305 |
+| `discordWebhook` | `apiDiscordWebhook` | admin.js:1308 |
+| `githubToken` | `apiGithubToken` | admin.js:1312 |
+| `customApi1Name` | `apiCustom1Name` | admin.js:1315 |
+| `customApi1Key` | `apiCustom1Key` | admin.js:1316 |
+| `customApi1Url` | `apiCustom1URL` | admin.js:1317 |
+| `customApi2Name` | `apiCustom2Name` | admin.js:1320 |
+| `customApi2Key` | `apiCustom2Key` | admin.js:1321 |
+| `customApi2Url` | `apiCustom2URL` | admin.js:1322 |
+| `customApi3Name` | `apiCustom3Name` | admin.js:1325 |
+| `customApi3Key` | `apiCustom3Key` | admin.js:1326 |
+| `customApi3Url` | `apiCustom3URL` | admin.js:1327 |
+
+**Fix:** In `admin.js`, rename all to match HTML IDs (add `api` prefix, use `URL` not `Url`). OR rename all HTML IDs to match JS convention.
+
+#### 1.1.4 Theme Section (7 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `themeAccent` | `themeAccentColor` | admin.js:1351+ |
+| `themeAccentHex` | `themeAccentColorHex` | admin.js:1351+ |
+| `themeBg` | `themeBgColor` | admin.js:1351+ |
+| `themeBgHex` | `themeBgColorHex` | admin.js:1351+ |
+| `themeText` | `themeTextColor` | admin.js:1351+ |
+| `themeTextHex` | `themeTextColorHex` | admin.js:1351+ |
+| `themeGrid` | `themeGridBg` | admin.js:1351+ |
+| `valFontSize` | `themeFontSizeVal` | admin.js:1351+ |
+
+**Fix:** In `admin.js`, add `Color` suffix to colour IDs, `themeGrid` → `themeGridBg`, `valFontSize` → `themeFontSizeVal`.
+
+#### 1.1.5 Services Section (2 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `managedServicesList` | `servicesList` | admin.js:1009 |
+| `serviceUrl` | `serviceURL` | admin.js:1034 |
+
+**Fix:** `managedServicesList` → `servicesList`, `serviceUrl` → `serviceURL`.
+
+#### 1.1.6 Navigation Section (2 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `navItemsList` | `navList` | admin.js:1089 |
+| `navUrl` | `navURL` | admin.js:1118 |
+
+**Fix:** `navItemsList` → `navList`, `navUrl` → `navURL`.
+
+#### 1.1.7 Links Section (2 mismatches)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `managedLinksList` | `linksList` | admin.js:1175 |
+| `linkUrl` | `linkURL` | admin.js:1201 |
+
+**Fix:** `managedLinksList` → `linksList`, `linkUrl` → `linkURL`.
+
+#### 1.1.8 Other Mismatches (4)
+| admin.js uses | admin.html has | File:Line (JS) |
+|---|---|---|
+| `loginForm` | *(no form element)* | admin.js:29 |
+| `notifications` | `notification` | admin.js:259 |
+| `trackUrl` | `trackURL` | admin.js:826+ |
+| *(none)* | `editorWordCount` (JS-created #bpmValue in post.js) | — |
+
+**Fix:** Either add `
-
+
@@ -640,7 +640,7 @@
-
+
@@ -681,7 +681,7 @@
-
+
@@ -958,7 +958,7 @@
-
+
@@ -993,7 +993,7 @@
-
+
diff --git a/api/app.py b/api/app.py
index 31fedba..d1b006a 100644
--- a/api/app.py
+++ b/api/app.py
@@ -2,6 +2,7 @@
"""JAESWIFT HUD Backend API"""
import json, os, time, subprocess, random, datetime, hashlib, zipfile, io, smtplib
from functools import wraps
+from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
@@ -10,6 +11,8 @@ from flask import Flask, request, jsonify, abort, send_file
from flask_cors import CORS
import jwt
import requests as req
+import urllib3
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
app = Flask(__name__)
CORS(app)
@@ -23,6 +26,23 @@ ARRAY_FILES = {
'posts.json', 'tracks.json', 'navigation.json', 'links.json',
'managed_services.json', 'messages.json'
}
+# ─── JSON Error Handlers ─────────────────────────────
+@app.errorhandler(400)
+def bad_request(e):
+ return jsonify({'error': 'Bad Request', 'message': str(e.description) if hasattr(e, 'description') else str(e)}), 400
+
+@app.errorhandler(401)
+def unauthorized(e):
+ return jsonify({'error': 'Unauthorized', 'message': str(e.description) if hasattr(e, 'description') else str(e)}), 401
+
+@app.errorhandler(404)
+def not_found(e):
+ return jsonify({'error': 'Not Found', 'message': str(e.description) if hasattr(e, 'description') else str(e)}), 404
+
+@app.errorhandler(500)
+def server_error(e):
+ return jsonify({'error': 'Internal Server Error', 'message': str(e.description) if hasattr(e, 'description') else str(e)}), 500
+
# ─── Helpers ─────────────────────────────────────────
def load_json(name):
@@ -188,15 +208,20 @@ def services():
{'name': 'Agent Zero', 'url': 'https://agentzero.jaeswift.xyz'},
{'name': 'Files', 'url': 'https://files.jaeswift.xyz'},
]
- results = []
- for s in svcs:
+ def check_service(s):
try:
t0 = time.time()
- r = req.get(s['url'], timeout=5, verify=False, allow_redirects=True)
+ r = req.get(s['url'], timeout=2, verify=False, allow_redirects=True)
ms = round((time.time() - t0) * 1000)
- results.append({**s, 'status': 'online' if r.status_code < 500 else 'offline', 'response_time_ms': ms})
+ return {**s, 'status': 'online' if r.status_code < 500 else 'offline', 'response_time_ms': ms}
except Exception:
- results.append({**s, 'status': 'offline', 'response_time_ms': 0})
+ return {**s, 'status': 'offline', 'response_time_ms': 0}
+
+ results = [None] * len(svcs)
+ with ThreadPoolExecutor(max_workers=7) as executor:
+ futures = {executor.submit(check_service, s): i for i, s in enumerate(svcs)}
+ for future in as_completed(futures):
+ results[futures[future]] = future.result()
return jsonify(results)
# ─── Weather ─────────────────────────────────────────
@@ -261,11 +286,11 @@ def delete_track(index):
def git_activity():
try:
r = req.get('https://git.jaeswift.xyz/api/v1/users/jae/heatmap',
- timeout=5, verify=False)
+ timeout=2, verify=False)
heatmap = r.json() if r.status_code == 200 else []
r2 = req.get('https://git.jaeswift.xyz/api/v1/repos/search?sort=updated&limit=5&owner=jae',
- timeout=5, verify=False)
+ timeout=2, verify=False)
repos = []
if r2.status_code == 200:
data = r2.json().get('data', r2.json()) if isinstance(r2.json(), dict) else r2.json()
diff --git a/css/admin.css b/css/admin.css
index 4ee4998..e498db5 100644
--- a/css/admin.css
+++ b/css/admin.css
@@ -1980,3 +1980,505 @@ body {
.gap-sm { gap: 0.4rem; }
.gap-md { gap: 0.8rem; }
.gap-lg { gap: 1.2rem; }
+
+/* ===================================================
+ MISSING CLASSES — Added by audit fix 2026-04-01
+ =================================================== */
+
+/* ─── LAYOUT: Sidebar ─── */
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 240px;
+ height: 100vh;
+ background: #060a12;
+ border-right: 1px solid rgba(0,255,200,0.1);
+ display: flex;
+ flex-direction: column;
+ z-index: 1000;
+ transition: transform 0.3s ease;
+ overflow-y: auto;
+}
+.sidebar-brand {
+ padding: 1.2rem 1rem;
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: #00ffc8;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ border-bottom: 1px solid rgba(0,255,200,0.08);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+.sidebar-close {
+ display: none;
+ position: absolute;
+ top: 0.8rem;
+ right: 0.8rem;
+ background: none;
+ border: 1px solid rgba(0,255,200,0.2);
+ color: #00ffc8;
+ font-size: 1.2rem;
+ cursor: pointer;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+}
+.sidebar-divider {
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0,255,200,0.15), transparent);
+ margin: 0.5rem 0;
+}
+.sidebar-header {
+ padding: 0.6rem 1rem 0.3rem;
+ font-size: 0.6rem;
+ color: rgba(0,255,200,0.4);
+ text-transform: uppercase;
+ letter-spacing: 3px;
+}
+.sidebar-nav {
+ flex: 1;
+ padding: 0.5rem 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.sidebar-section-label {
+ padding: 0.8rem 1rem 0.3rem;
+ font-size: 0.55rem;
+ color: rgba(0,255,200,0.35);
+ text-transform: uppercase;
+ letter-spacing: 3px;
+ font-weight: 600;
+}
+.sidebar-version {
+ padding: 0.8rem 1rem;
+ font-size: 0.6rem;
+ color: rgba(255,255,255,0.2);
+ border-top: 1px solid rgba(0,255,200,0.06);
+ text-align: center;
+}
+
+/* ─── LAYOUT: Topbar ─── */
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.8rem 1.5rem;
+ background: rgba(6,10,18,0.9);
+ border-bottom: 1px solid rgba(0,255,200,0.08);
+ backdrop-filter: blur(10px);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+.topbar-title {
+ font-size: 0.75rem;
+ color: rgba(0,255,200,0.6);
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-weight: 600;
+}
+.topbar-logout {
+ background: none;
+ border: 1px solid rgba(255,50,50,0.3);
+ color: #ff3232;
+ padding: 0.4rem 1rem;
+ font-size: 0.65rem;
+ cursor: pointer;
+ border-radius: 4px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ transition: all 0.2s;
+}
+.topbar-logout:hover {
+ background: rgba(255,50,50,0.1);
+ border-color: #ff3232;
+}
+
+/* ─── AUTH: Login ─── */
+.login-screen {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ background: #060a12;
+}
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+}
+.login-logo {
+ text-align: center;
+ margin-bottom: 1.5rem;
+ font-size: 1.8rem;
+ color: #00ffc8;
+ text-shadow: 0 0 20px rgba(0,255,200,0.3);
+ letter-spacing: 4px;
+ font-weight: 700;
+}
+
+/* ─── UI: Notification ─── */
+.notification {
+ position: fixed;
+ top: 1rem;
+ right: 1rem;
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ pointer-events: none;
+}
+.notification > * {
+ pointer-events: auto;
+}
+
+/* ─── EDITOR: Operator HUD ─── */
+.operator-hud {
+ background: rgba(0,255,200,0.03);
+ border: 1px solid rgba(0,255,200,0.1);
+ border-radius: 8px;
+ padding: 1rem;
+ margin-top: 1rem;
+}
+.hud-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 0.8rem;
+ margin-top: 0.8rem;
+}
+.hud-range {
+ width: 100%;
+ accent-color: #00ffc8;
+ height: 4px;
+ cursor: pointer;
+}
+.hud-val {
+ display: inline-block;
+ min-width: 24px;
+ text-align: center;
+ color: #00ffc8;
+ font-weight: 700;
+ font-size: 0.9rem;
+ font-family: "JetBrains Mono", monospace;
+}
+
+/* ─── EDITOR: Split view ─── */
+.editor-split {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ min-height: 400px;
+}
+.editor-pane {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+.preview-pane {
+ background: rgba(0,0,0,0.3);
+ border: 1px solid rgba(0,255,200,0.08);
+ border-radius: 6px;
+ padding: 1rem;
+ overflow-y: auto;
+ max-height: 600px;
+ color: #e0e0e0;
+ font-size: 0.85rem;
+ line-height: 1.6;
+}
+.editor-meta-bar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.8rem;
+ align-items: end;
+ padding: 0.8rem 0;
+ border-bottom: 1px solid rgba(0,255,200,0.06);
+ margin-bottom: 0.8rem;
+}
+.editor-textarea-sm {
+ width: 100%;
+ min-height: 60px;
+ resize: vertical;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid rgba(0,255,200,0.12);
+ border-radius: 4px;
+ color: #e0e0e0;
+ padding: 0.5rem;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.8rem;
+}
+
+/* ─── EDITOR: Toolbar ─── */
+.section-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.5rem;
+ background: rgba(0,0,0,0.2);
+ border: 1px solid rgba(0,255,200,0.06);
+ border-radius: 6px;
+ margin-bottom: 0.8rem;
+}
+.toolbar-btn {
+ background: rgba(0,255,200,0.06);
+ border: 1px solid rgba(0,255,200,0.12);
+ color: #00ffc8;
+ padding: 0.3rem 0.6rem;
+ font-size: 0.7rem;
+ cursor: pointer;
+ border-radius: 3px;
+ transition: all 0.2s;
+ font-family: "JetBrains Mono", monospace;
+}
+.toolbar-btn:hover {
+ background: rgba(0,255,200,0.15);
+ border-color: #00ffc8;
+}
+.toolbar-sep {
+ width: 1px;
+ height: 20px;
+ background: rgba(0,255,200,0.12);
+ margin: 0 0.2rem;
+}
+
+/* ─── POSTS: Table ─── */
+.admin-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.75rem;
+}
+.admin-table th,
+.admin-table td {
+ padding: 0.6rem 0.8rem;
+ text-align: left;
+ border-bottom: 1px solid rgba(0,255,200,0.06);
+}
+.admin-table th {
+ color: rgba(0,255,200,0.5);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-size: 0.6rem;
+ font-weight: 600;
+}
+.admin-table tr:hover {
+ background: rgba(0,255,200,0.03);
+}
+.table-wrap {
+ overflow-x: auto;
+ border: 1px solid rgba(0,255,200,0.08);
+ border-radius: 6px;
+ background: rgba(0,0,0,0.15);
+}
+
+/* ─── TRACKS ─── */
+.tracks-add-form {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 0.8rem;
+ padding: 1rem;
+ background: rgba(0,255,200,0.02);
+ border: 1px solid rgba(0,255,200,0.08);
+ border-radius: 6px;
+ margin-bottom: 1rem;
+}
+.tracks-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+
+/* ─── SETTINGS ─── */
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1rem;
+}
+.setting-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.8rem 1rem;
+ background: rgba(0,0,0,0.2);
+ border: 1px solid rgba(0,255,200,0.06);
+ border-radius: 6px;
+ gap: 1rem;
+}
+.toggle-slider {
+ position: relative;
+ width: 42px;
+ height: 22px;
+ background: rgba(255,255,255,0.1);
+ border-radius: 11px;
+ cursor: pointer;
+ transition: background 0.3s;
+ flex-shrink: 0;
+}
+.toggle-slider::after {
+ content: "";
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 18px;
+ height: 18px;
+ background: #888;
+ border-radius: 50%;
+ transition: all 0.3s;
+}
+.toggle-slider.active {
+ background: rgba(0,255,200,0.2);
+}
+.toggle-slider.active::after {
+ left: 22px;
+ background: #00ffc8;
+}
+
+/* ─── HOMEPAGE ─── */
+.homepage-section-name {
+ font-size: 0.7rem;
+ color: #00ffc8;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-weight: 600;
+}
+.homepage-section-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* ─── SERVICES / NAV / LINKS manage lists ─── */
+.services-manage-list,
+.nav-manage-list,
+.links-manage-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ margin-top: 0.8rem;
+}
+.services-manage-list > div,
+.nav-manage-list > div,
+.links-manage-list > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.6rem 0.8rem;
+ background: rgba(0,0,0,0.2);
+ border: 1px solid rgba(0,255,200,0.06);
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+/* ─── API KEYS ─── */
+.apikey-group {
+ background: rgba(0,0,0,0.2);
+ border: 1px solid rgba(0,255,200,0.08);
+ border-radius: 8px;
+ padding: 1.2rem;
+ margin-bottom: 1rem;
+}
+
+/* ─── THEME ─── */
+.color-input {
+ width: 50px;
+ height: 36px;
+ border: 1px solid rgba(0,255,200,0.15);
+ border-radius: 4px;
+ cursor: pointer;
+ background: transparent;
+ padding: 2px;
+}
+.color-input::-webkit-color-swatch-wrapper { padding: 0; }
+.color-input::-webkit-color-swatch { border: none; border-radius: 2px; }
+.color-hex {
+ width: 90px;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.75rem;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid rgba(0,255,200,0.12);
+ border-radius: 4px;
+ color: #e0e0e0;
+ padding: 0.4rem 0.5rem;
+ text-align: center;
+}
+
+/* ─── UTILITY: Size variants ─── */
+.sm { font-size: 0.75rem; }
+.lg { font-size: 1.1rem; }
+
+/* ─── BACKUPS ─── */
+.backup-card-icon {
+ font-size: 1.8rem;
+ margin-bottom: 0.5rem;
+}
+.backup-card-title {
+ font-size: 0.8rem;
+ font-weight: 700;
+ color: #00ffc8;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: 0.3rem;
+}
+.backup-card-desc {
+ font-size: 0.65rem;
+ color: rgba(255,255,255,0.4);
+ line-height: 1.4;
+}
+.backup-desc {
+ font-size: 0.7rem;
+ color: rgba(255,255,255,0.5);
+ padding: 0.8rem 0;
+ line-height: 1.5;
+}
+.backup-export {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.8rem;
+ margin-top: 0.8rem;
+}
+.backup-status {
+ padding: 0.8rem;
+ background: rgba(0,255,200,0.03);
+ border: 1px solid rgba(0,255,200,0.08);
+ border-radius: 6px;
+ font-size: 0.7rem;
+ color: rgba(255,255,255,0.5);
+ margin-top: 0.8rem;
+}
+
+/* ─── DASHBOARD: Threat Feed ─── */
+.threat-feed {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ margin-top: 0.5rem;
+}
+.threat-feed .threat-empty {
+ padding: 1rem;
+ text-align: center;
+ color: #00ffc8;
+ font-size: 0.75rem;
+ opacity: 0.6;
+}
+
+/* ─── RESPONSIVE: Sidebar mobile ─── */
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ }
+ .sidebar.open {
+ transform: translateX(0);
+ }
+ .sidebar-close {
+ display: block;
+ }
+ .editor-split {
+ grid-template-columns: 1fr;
+ }
+ .hud-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+}
diff --git a/js/admin.js b/js/admin.js
index 4a9faed..ca8b1c4 100644
--- a/js/admin.js
+++ b/js/admin.js
@@ -63,13 +63,13 @@ const AdminApp = {
}
// Editor: auto-slug on title input
- const editTitle = document.getElementById('editTitle');
+ const editTitle = document.getElementById('editorTitle');
if (editTitle) {
editTitle.addEventListener('input', () => this.autoSlug());
}
// Editor: live preview on content input
- const editContent = document.getElementById('editContent');
+ const editContent = document.getElementById('editorContent');
if (editContent) {
editContent.addEventListener('input', () => this.updatePreview());
}
@@ -82,10 +82,10 @@ const AdminApp = {
});
// Operator HUD sliders
- ['editEnergy', 'editMotivation', 'editFocus', 'editDifficulty'].forEach(id => {
+ ['hudEnergy', 'hudMotivation', 'hudFocus', 'hudDifficulty'].forEach(id => {
const slider = document.getElementById(id);
if (slider) {
- const valId = 'val' + id.replace('edit', '');
+ const valId = 'val' + id.replace('hud', '');
const valEl = document.getElementById(valId);
if (valEl) valEl.textContent = slider.value;
slider.addEventListener('input', () => {
@@ -95,7 +95,7 @@ const AdminApp = {
});
// Theme colour sync
- ['Accent', 'Bg', 'Text'].forEach(name => {
+ ['AccentColor', 'BgColor', 'TextColor'].forEach(name => {
const picker = document.getElementById('theme' + name);
const hex = document.getElementById('theme' + name + 'Hex');
if (picker && hex) {
@@ -112,7 +112,7 @@ const AdminApp = {
// Theme font size slider
const fontSlider = document.getElementById('themeFontSize');
- const valFont = document.getElementById('valFontSize');
+ const valFont = document.getElementById('themeFontSizeVal');
if (fontSlider && valFont) {
valFont.textContent = fontSlider.value;
fontSlider.addEventListener('input', () => {
@@ -256,7 +256,7 @@ const AdminApp = {
/* ─────────────────── NOTIFICATIONS ─────────────────── */
notify(msg, type = 'success') {
- const container = document.getElementById('notifications') || document.body;
+ const container = document.getElementById('notification') || document.body;
const div = document.createElement('div');
div.className = 'notification notification-' + type;
div.innerHTML = `${type === 'success' ? '✓' : type === 'error' ? '✗' : 'ℹ'}${msg}`;
@@ -303,52 +303,58 @@ const AdminApp = {
/* ─────────────────── 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() })
- ]);
+ 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
- if (statsRes.ok) {
- const stats = await statsRes.json();
- const cpu = document.getElementById('dashCPU');
- const mem = document.getElementById('dashMEM');
- const disk = document.getElementById('dashDISK');
+ // 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
- if (postsRes.ok) {
- const posts = await postsRes.json();
+ // 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('dashPosts');
- const dashWords = document.getElementById('dashWords');
+ 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
- if (tracksRes.ok) {
- const tracks = await tracksRes.json();
+ // 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('dashTracks');
+ const dashTracks = document.getElementById('statTracks');
if (dashTracks) dashTracks.textContent = arr.length;
}
+ } catch (e) { console.warn('Dashboard tracks error:', e); }
- // Services grid
- if (servicesRes.ok) {
- const services = await servicesRes.json();
+ // 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('dashServicesGrid');
+ const grid = document.getElementById('servicesGrid');
if (grid) {
grid.innerHTML = arr.map(svc => {
const isUp = svc.status === 'up' || svc.status === 'online' || svc.status === true;
@@ -361,16 +367,21 @@ const AdminApp = {
`;
}).join('');
}
+ } else {
+ const grid = document.getElementById('servicesGrid');
+ if (grid) grid.innerHTML = 'Services check timed out
';
}
+ } catch (e) { console.warn('Dashboard services error:', e); }
- // Threats
- if (threatsRes.ok) {
- const threats = await threatsRes.json();
+ // 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 ✓
';
+ container.innerHTML = 'No active threats detected \u2713
';
} else {
container.innerHTML = arr.map(t => {
const severity = (t.severity || t.level || 'low').toLowerCase();
@@ -386,10 +397,7 @@ const AdminApp = {
}
}
}
-
- } catch (err) {
- console.error('Dashboard load error:', err);
- }
+ } catch (e) { console.warn('Dashboard threats error:', e); }
},
/* ─────────────────── POSTS ─────────────────── */
@@ -448,35 +456,35 @@ const AdminApp = {
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 || '';
+ 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('editDate').value = dateStr;
- document.getElementById('editTime').value = timeStr;
+ document.getElementById('editorDate').value = dateStr;
+ document.getElementById('editorTime').value = timeStr;
} else {
- document.getElementById('editDate').value = '';
- document.getElementById('editTime').value = '';
+ document.getElementById('editorDate').value = '';
+ document.getElementById('editorTime').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 || '';
+ 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('editMood');
+ const moodEl = document.getElementById('hudMood');
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' }
+ { 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);
@@ -487,13 +495,13 @@ const AdminApp = {
}
});
- const coffeeEl = document.getElementById('editCoffee');
+ const coffeeEl = document.getElementById('hudCoffee');
if (coffeeEl) coffeeEl.value = post.coffee || 0;
- const bpmEl = document.getElementById('editBPM');
+ const bpmEl = document.getElementById('hudBPM');
if (bpmEl) bpmEl.value = post.bpm || 0;
- const threatEl = document.getElementById('editThreat');
+ const threatEl = document.getElementById('hudThreat');
if (threatEl) threatEl.value = post.threat_level || 'low';
this.updatePreview();
@@ -521,14 +529,14 @@ const AdminApp = {
},
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;
+ 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; }
@@ -544,14 +552,14 @@ const AdminApp = {
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 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,
@@ -587,37 +595,37 @@ const AdminApp = {
},
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 = '';
+ 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('editMood');
+ const moodEl = document.getElementById('hudMood');
if (moodEl) moodEl.value = 'focused';
- ['editEnergy', 'editMotivation', 'editFocus', 'editDifficulty'].forEach(id => {
+ ['hudEnergy', 'hudMotivation', 'hudFocus', 'hudDifficulty'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.value = 3;
- const valId = 'val' + id.replace('edit', '');
+ const valId = 'val' + id.replace('hud', '');
const valEl = document.getElementById(valId);
if (valEl) valEl.textContent = '3';
}
});
- const coffeeEl = document.getElementById('editCoffee');
+ const coffeeEl = document.getElementById('hudCoffee');
if (coffeeEl) coffeeEl.value = 0;
- const bpmEl = document.getElementById('editBPM');
+ const bpmEl = document.getElementById('hudBPM');
if (bpmEl) bpmEl.value = 80;
- const threatEl = document.getElementById('editThreat');
+ const threatEl = document.getElementById('hudThreat');
if (threatEl) threatEl.value = 'low';
const heading = document.getElementById('editorTitle');
@@ -627,7 +635,7 @@ const AdminApp = {
/* ─────────────────── EDITOR TOOLBAR ─────────────────── */
insertMarkdown(action) {
- const textarea = document.getElementById('editContent');
+ const textarea = document.getElementById('editorContent');
if (!textarea) return;
const start = textarea.selectionStart;
@@ -700,7 +708,7 @@ const AdminApp = {
},
updatePreview() {
- const content = document.getElementById('editContent')?.value || '';
+ const content = document.getElementById('editorContent')?.value || '';
const preview = document.getElementById('editorPreview');
if (!preview) return;
@@ -758,7 +766,7 @@ const AdminApp = {
},
autoSlug() {
- const title = document.getElementById('editTitle')?.value || '';
+ const title = document.getElementById('editorTitle')?.value || '';
const slug = title
.toLowerCase()
.trim()
@@ -766,7 +774,7 @@ const AdminApp = {
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
- const slugEl = document.getElementById('editSlug');
+ const slugEl = document.getElementById('editorSlug');
if (slugEl) slugEl.value = slug;
},
@@ -812,7 +820,7 @@ const AdminApp = {
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 url = document.getElementById('trackURL')?.value.trim();
const cover = document.getElementById('trackCover')?.value.trim();
if (!title || !artist) {
@@ -832,7 +840,7 @@ const AdminApp = {
this.notify('Track added');
// Clear inputs
- ['trackTitle', 'trackArtist', 'trackAlbum', 'trackGenre', 'trackUrl', 'trackCover'].forEach(id => {
+ ['trackTitle', 'trackArtist', 'trackAlbum', 'trackGenre', 'trackURL', 'trackCover'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
@@ -1006,7 +1014,7 @@ const AdminApp = {
const data = await res.json();
this.servicesData = Array.isArray(data) ? data : (data.services || []);
- const container = document.getElementById('managedServicesList');
+ const container = document.getElementById('servicesList');
if (!container) return;
if (this.servicesData.length === 0) {
@@ -1031,7 +1039,7 @@ const AdminApp = {
async addService() {
const name = document.getElementById('serviceName')?.value.trim();
- const url = document.getElementById('serviceUrl')?.value.trim();
+ const url = document.getElementById('serviceURL')?.value.trim();
if (!name) {
this.notify('Service name is required', 'error');
@@ -1044,12 +1052,12 @@ const AdminApp = {
const res = await fetch(this.API + '/services/managed', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.servicesData)
+ 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 = '';
+ document.getElementById('serviceURL').value = '';
this.loadServices();
} catch (err) {
console.error('Add service error:', err);
@@ -1066,7 +1074,7 @@ const AdminApp = {
const res = await fetch(this.API + '/services/managed', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.servicesData)
+ body: JSON.stringify({ name, url })
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Service deleted');
@@ -1086,7 +1094,7 @@ const AdminApp = {
const data = await res.json();
this.navData = Array.isArray(data) ? data : (data.items || []);
- const container = document.getElementById('navItemsList');
+ const container = document.getElementById('navList');
if (!container) return;
if (this.navData.length === 0) {
@@ -1115,7 +1123,7 @@ const AdminApp = {
async addNavItem() {
const label = document.getElementById('navLabel')?.value.trim();
- const url = document.getElementById('navUrl')?.value.trim();
+ const url = document.getElementById('navURL')?.value.trim();
const order = parseInt(document.getElementById('navOrder')?.value || 0);
if (!label || !url) {
@@ -1129,12 +1137,12 @@ const AdminApp = {
const res = await fetch(this.API + '/navigation', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.navData)
+ 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('navURL').value = '';
document.getElementById('navOrder').value = '';
this.loadNavigation();
} catch (err) {
@@ -1152,7 +1160,7 @@ const AdminApp = {
const res = await fetch(this.API + '/navigation', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.navData)
+ body: JSON.stringify({ label, url, order })
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Nav item deleted');
@@ -1172,7 +1180,7 @@ const AdminApp = {
const data = await res.json();
this.linksData = Array.isArray(data) ? data : (data.links || []);
- const container = document.getElementById('managedLinksList');
+ const container = document.getElementById('linksList');
if (!container) return;
if (this.linksData.length === 0) {
@@ -1198,7 +1206,7 @@ const AdminApp = {
async addLink() {
const name = document.getElementById('linkName')?.value.trim();
- const url = document.getElementById('linkUrl')?.value.trim();
+ const url = document.getElementById('linkURL')?.value.trim();
const icon = document.getElementById('linkIcon')?.value.trim();
const category = document.getElementById('linkCategory')?.value.trim();
@@ -1213,11 +1221,11 @@ const AdminApp = {
const res = await fetch(this.API + '/links', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.linksData)
+ 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 => {
+ ['linkName', 'linkURL', 'linkIcon', 'linkCategory'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
@@ -1237,7 +1245,7 @@ const AdminApp = {
const res = await fetch(this.API + '/links', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(this.linksData)
+ body: JSON.stringify({ name, url, icon: icon || '🔗', category: category || 'general' })
});
if (!res.ok) throw new Error('Delete failed');
this.notify('Link deleted');
@@ -1257,28 +1265,28 @@ const AdminApp = {
const data = await res.json();
// Weather
- this.setVal('weatherApiKey', data.weather_api_key);
+ this.setVal('apiWeatherKey', 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);
+ this.setVal('apiSpotifyClientId', data.spotify_client_id);
+ this.setVal('apiSpotifyClientSecret', data.spotify_client_secret);
+ this.setVal('apiSpotifyRefreshToken', 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);
+ 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('discordWebhook', data.discord_webhook);
- this.setVal('githubToken', data.github_token);
+ this.setVal('apiDiscordWebhook', data.discord_webhook);
+ this.setVal('apiGithubToken', 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`]);
+ 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);
@@ -1287,43 +1295,43 @@ const AdminApp = {
},
async saveApiKey(group) {
- const payload = { group };
+ const data = {};
switch (group) {
case 'weather':
- payload.weather_api_key = this.getVal('weatherApiKey');
+ data.api_key = this.getVal('apiWeatherKey');
break;
case 'spotify':
- payload.spotify_client_id = this.getVal('spotifyClientId');
- payload.spotify_client_secret = this.getVal('spotifyClientSecret');
- payload.spotify_refresh_token = this.getVal('spotifyRefreshToken');
+ data.client_id = this.getVal('apiSpotifyClientId');
+ data.client_secret = this.getVal('apiSpotifyClientSecret');
+ data.refresh_token = this.getVal('apiSpotifyRefreshToken');
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');
+ data.host = this.getVal('apiSmtpHost');
+ data.port = this.getVal('apiSmtpPort');
+ data.user = this.getVal('apiSmtpUser');
+ data.pass = this.getVal('apiSmtpPass');
break;
case 'discord':
- payload.discord_webhook = this.getVal('discordWebhook');
+ data.webhook = this.getVal('apiDiscordWebhook');
break;
case 'github':
- payload.github_token = this.getVal('githubToken');
+ data.token = this.getVal('apiGithubToken');
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');
+ data.name = this.getVal('apiCustom1Name');
+ data.key = this.getVal('apiCustom1Key');
+ data.url = this.getVal('apiCustom1URL');
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');
+ data.name = this.getVal('apiCustom2Name');
+ data.key = this.getVal('apiCustom2Key');
+ data.url = this.getVal('apiCustom2URL');
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');
+ 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');
@@ -1334,7 +1342,7 @@ const AdminApp = {
const res = await fetch(this.API + '/apikeys', {
method: 'POST',
headers: this.authHeaders(),
- body: JSON.stringify(payload)
+ body: JSON.stringify({ group, data })
});
if (!res.ok) throw new Error('Save failed');
this.notify('API keys saved (' + group + ')');
@@ -1353,23 +1361,23 @@ const AdminApp = {
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);
+ 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('themeGrid', data.grid !== undefined ? data.grid : this.themeDefaults.grid);
+ 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('valFontSize');
+ const valFont = document.getElementById('themeFontSizeVal');
if (valFont) valFont.textContent = fontSize;
} catch (err) {
console.error('Theme load error:', err);
@@ -1379,13 +1387,13 @@ const AdminApp = {
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'),
+ 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('themeGrid'),
+ grid: this.getChecked('themeGridBg'),
font_size: parseInt(this.getVal('themeFontSize') || 16)
};
@@ -1406,18 +1414,18 @@ const AdminApp = {
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.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('themeGrid', this.themeDefaults.grid);
+ this.setChecked('themeGridBg', this.themeDefaults.grid);
this.setVal('themeFontSize', this.themeDefaults.fontSize);
- const valFont = document.getElementById('valFontSize');
+ const valFont = document.getElementById('themeFontSizeVal');
if (valFont) valFont.textContent = this.themeDefaults.fontSize;
this.notify('Theme reset to defaults — save to apply', 'info');