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 `
` wrapper in HTML or change JS to use `loginBtn` click handler only. `notifications` → `notification`, `trackUrl` → `trackURL`. + +--- + +### 1.2 ❌ CRITICAL: 5 Undefined onclick Methods +**Impact:** Clicking Save buttons in 5 admin sections throws `AdminApp.X is not a function` error. + +| Method Called (admin.html) | Correct Method in admin.js | HTML Line | +|---|---|---| +| `AdminApp.saveServices()` | *(does not exist)* | admin.html:608 | +| `AdminApp.saveNavigation()` | *(does not exist)* | admin.html:643 | +| `AdminApp.saveLinks()` | *(does not exist)* | admin.html:684 | +| `AdminApp.saveSEO()` | *(does not exist)* | admin.html:961 | +| `AdminApp.saveContact()` | `AdminApp.saveContactSettings()` | admin.html:996 | + +**Fix for saveContact:** Change `admin.html:996` from `AdminApp.saveContact()` to `AdminApp.saveContactSettings()`. + +**Fix for saveServices/saveNavigation/saveLinks/saveSEO:** These methods need to be implemented in `admin.js`. Currently, the admin.js CRUD for services/nav/links works per-item (add/delete) but has no bulk "Save" method. Either: +- (a) Remove the Save buttons from HTML (since add/delete already persist via API), OR +- (b) Add PUT methods in admin.js that POST the full array + +For saveSEO, admin.js has `saveSeo()` (lowercase 'eo'). Change `admin.html:961` to `AdminApp.saveSeo()`. + +--- + +### 1.3 ❌ CRITICAL: API Payload Shape Mismatches (Frontend ↔ Backend) +**Impact:** POST requests from admin panel silently fail or corrupt data. + +#### 1.3.1 Services/Navigation/Links — Array vs Single Object +- **admin.js `addService()`** (line 1047): Sends `JSON.stringify(this.servicesData)` — the **full array** +- **app.py `add_managed_service()`** (line 342): Does `d = request.get_json(); svcs.append({name: d.get('name')})` — expects a **single object** +- Result: Flask receives an array, calls `.get('name')` on it → returns `None`, appends `{name: '', url: ''}` empty entry + +Same pattern for: +- `addNavItem()` (admin.js:1129) vs `add_navigation()` (app.py:374) +- `addLink()` (admin.js:1213) vs `add_link()` (app.py:409) + +**Fix:** Change admin.js to send only the new item: +```javascript +// In addService(): +body: JSON.stringify({ name, url }) // NOT this.servicesData +``` + +#### 1.3.2 API Keys — Flat vs Nested Payload +- **admin.js `saveApiKey()`** (line 1334): Sends `{ group: 'weather', weather_api_key: 'xxx' }` — flat object with group field +- **app.py `save_apikeys()`** (line 474-476): Expects `{ group: 'weather', data: { api_key: 'xxx' } }` — nested with `data` dict +- Result: `data = d.get('data', {})` returns empty dict → nothing saved + +**Fix:** Change admin.js `saveApiKey()` to wrap fields in a `data` object: +```javascript +const payload = { group }; +const data = {}; +switch (group) { + case 'weather': data.api_key = this.getVal('weatherApiKey'); break; + // ... +} +payload.data = data; +``` + +--- + +### 1.4 ❌ CRITICAL: /api/services Endpoint Hangs (Timeout) +**File:** `api/app.py:181-200` +**Impact:** Dashboard never finishes loading. Nginx logs show `upstream timed out (110: Connection timed out)`. The admin dashboard calls this at load time via `loadDashboard()`. + +**Root Cause:** The `services()` function makes synchronous HTTP requests to 7 external URLs (git.jaeswift.xyz, plex.jaeswift.xyz, etc.) from inside the Flask app. The VPS cannot resolve its own subdomains from localhost — `curl -sk https://git.jaeswift.xyz` returns code 000 (DNS/connection failure) with 3+ second timeout per service. Total: 7 × 5s timeout = 35s, exceeding nginx's default 60s proxy timeout. + +**Fix:** +1. Add `/etc/hosts` entries on VPS: `127.0.0.1 git.jaeswift.xyz plex.jaeswift.xyz archive.jaeswift.xyz` +2. OR reduce timeout in app.py: `req.get(s['url'], timeout=2, ...)` +3. OR use `concurrent.futures.ThreadPoolExecutor` to check all services in parallel +4. OR move service checks to a background task/cache + +--- + +### 1.5 ❌ CRITICAL: /api/git-activity Endpoint Hangs +**File:** `api/app.py:260-282` +**Impact:** Same DNS issue as /api/services. Requests to `https://git.jaeswift.xyz/api/v1/users/jae/heatmap` fail from VPS localhost. + +**Fix:** Same as 1.4 — add hosts entries or reduce timeouts. + +--- + +### 1.6 ❌ CRITICAL: 51 CSS Classes Missing from admin.css +**Impact:** Major layout breakage in admin panel — sidebar, login screen, backups, settings, notifications, and many components render unstyled. + +**Missing classes referenced in admin.html but not defined in admin.css OR style.css:** + +| Class | Used In | Category | +|---|---|---| +| `.sidebar` | Layout wrapper | Layout | +| `.sidebar-brand` | Brand logo area | Layout | +| `.sidebar-close` | Mobile close btn | Layout | +| `.sidebar-divider` | Section dividers | Layout | +| `.sidebar-header` | Header area | Layout | +| `.sidebar-nav` | Nav container | Layout | +| `.sidebar-section-label` | Group labels | Layout | +| `.sidebar-version` | Version footer | Layout | +| `.topbar` | Top bar | Layout | +| `.topbar-title` | Title text | Layout | +| `.topbar-logout` | Logout btn | Layout | +| `.login-screen` | Login wrapper | Auth | +| `.login-form` | Form container | Auth | +| `.login-logo` | Logo area | Auth | +| `.notification` | Toast container | UI | +| `.operator-hud` | Editor HUD | Editor | +| `.hud-grid` | HUD metrics grid | Editor | +| `.hud-range` | Range sliders | Editor | +| `.hud-val` | Value labels | Editor | +| `.editor-split` | Split view | Editor | +| `.editor-pane` | Editor pane | Editor | +| `.preview-pane` | Preview pane | Editor | +| `.editor-meta-bar` | Meta info bar | Editor | +| `.editor-textarea-sm` | Small textareas | Editor | +| `.section-toolbar` | Toolbar | Editor | +| `.toolbar-btn` | Toolbar buttons | Editor | +| `.toolbar-sep` | Toolbar separator | Editor | +| `.admin-table` | Posts table | Posts | +| `.table-wrap` | Table wrapper | Posts | +| `.tracks-add-form` | Add track form | Tracks | +| `.tracks-list` | Tracks container | Tracks | +| `.settings-grid` | Settings layout | Settings | +| `.setting-item` | Setting row | Settings | +| `.toggle-slider` | Toggle control | Settings | +| `.homepage-section-name` | Section label | Homepage | +| `.homepage-section-controls` | Controls | Homepage | +| `.services-manage-list` | Services list | Services | +| `.nav-manage-list` | Nav items list | Navigation | +| `.links-manage-list` | Links list | Links | +| `.apikey-group` | Key group card | API Keys | +| `.color-input` | Colour picker | Theme | +| `.color-hex` | Hex input | Theme | +| `.sm` | Small variant | Utility | +| `.lg` | Large variant | Utility | +| `.backup-card-icon` | Card icon | Backups | +| `.backup-card-title` | Card title | Backups | +| `.backup-card-desc` | Card description | Backups | +| `.backup-desc` | Description text | Backups | +| `.backup-export` | Export section | Backups | +| `.backup-status` | Status area | Backups | +| `.threat-feed` | Threats container | Dashboard | + +**Fix:** These classes must be defined in `admin.css`. Many were apparently planned (the HTML uses them consistently) but never written. + +--- + +### 1.7 ❌ CRITICAL: Stale Root-Level JS Files on VPS +**Impact:** Potential confusion; the root-level `/var/www/jaeswift-homepage/admin.js` (66,567 bytes) is an OLDER version than `/var/www/jaeswift-homepage/js/admin.js` (66,766 bytes). The diff shows the root copy is missing the `showSection()` normalization fix. + +**Diff:** +```diff +# Root admin.js is MISSING these 3 lines present in js/admin.js: ++ // Normalize: ensure name has section- prefix ++ if (!name.startsWith('section-')) name = 'section-' + name; + +# Root admin.js has outdated sidebar active-link selector: +- const activeLink = document.querySelector(`[onclick*="'${name}'"]`); +# js/admin.js has fixed version: ++ const shortName = name.replace('section-', ''); ++ const activeLink = document.querySelector(`.sidebar-link[data-section="${shortName}"]`); +``` + +**Affected root files (permissions also wrong — 600 vs 644):** +| File | Root Size | js/ Size | Match? | +|---|---|---|---| +| admin.js | 66,567 (perms: 600) | 66,766 (perms: 644) | ❌ DIFFERENT | +| blog.js | 7,325 (perms: 600) | 7,325 (perms: 644) | ✅ Same | +| main.js | 36,092 (perms: 600) | 36,092 (perms: 644) | ✅ Same | +| post.js | 13,473 (perms: 600) | 13,473 (perms: 644) | ✅ Same | + +**Fix:** Delete the stale root-level JS files: +```bash +rm /var/www/jaeswift-homepage/admin.js /var/www/jaeswift-homepage/blog.js \ + /var/www/jaeswift-homepage/main.js /var/www/jaeswift-homepage/post.js +``` + +--- + +### 1.8 ❌ CRITICAL: showSection() Broken in Root admin.js +**File:** Root `/var/www/jaeswift-homepage/admin.js` (stale copy) +**Impact:** If somehow the root copy were served (e.g., nginx `try_files` fallback), the sidebar section switching would not work because the old code lacks the `section-` prefix normalization. + +Note: Currently nginx serves from `/js/admin.js` (the correct file), so this is only a risk if the stale file is served by accident. Still, it should be cleaned up. + +**Fix:** Delete root-level JS files (see 1.7). + +--- + +## 2. HIGH ISSUES + +### 2.1 ⚠️ HIGH: blog.html and post.html — 4 CSS Classes Undefined +**Impact:** Navbar, clock, and signal animation don't render correctly on blog pages. + +| Class | File | Not in | +|---|---|---| +| `.navbar` | blog.html, post.html | blog.css, style.css | +| `.nav-clock` | blog.html, post.html | blog.css, style.css | +| `.signal-flicker` | blog.html, post.html | blog.css, style.css | +| `.blog-header-content` | blog.html only | blog.css, style.css | + +**Fix:** Define these classes in `blog.css` or `style.css`. The index.html uses different class names for the navbar (likely defined in style.css under different names). + +### 2.2 ⚠️ HIGH: No CORS Headers on API +**Impact:** If any external/subdomain frontend tries to call the API, requests will be blocked. + +**Test result:** `curl -sI /api/posts | grep access-control` returned nothing. + +**Fix:** Add Flask-CORS or manual headers: +```python +from flask_cors import CORS +CORS(app, resources={r"/api/*": {"origins": "https://jaeswift.xyz"}}) +``` + +### 2.3 ⚠️ HIGH: Auth Endpoints Return HTML Error Pages +**File:** `api/app.py:41-52` (`require_auth` decorator) +**Impact:** Admin.js expects JSON responses but gets HTML `401 Unauthorized`. + +**Evidence:** +``` +curl http://127.0.0.1:5000/api/apikeys → +401 Unauthorized

Unauthorized

Missing token

+``` + +**Fix:** The `require_auth` decorator should use `abort()` with JSON, or Flask should be configured for JSON error responses: +```python +@app.errorhandler(401) +def unauthorized(e): + return jsonify({'error': 'Unauthorized', 'message': str(e)}), 401 +``` + +### 2.4 ⚠️ HIGH: /api/login Returns 404 (Route is /api/auth/login) +**Impact:** If anything calls `/api/login` (the intuitive URL), it 404s. + +**Analysis:** Flask route is `@app.route('/api/auth/login')` (line 62). admin.js correctly uses `this.API + '/auth/login'` (line 132). No functional issue currently, but the non-standard auth path could confuse future integrations. + +**Status:** Working correctly but unconventional. Consider adding an alias route. + +### 2.5 ⚠️ HIGH: admin.js Sends Full Array on Add Operations +**File:** `js/admin.js:1044-1048` (addService), `1129` (addNavItem), `1213` (addLink) +**Impact:** Even though the POST creates a garbage entry (see 1.3.1), the admin.js then calls `loadX()` which re-fetches from the API, showing the corrupted data. + +**Root Cause:** The JS pattern is: +1. Push new item to local array: `this.servicesData.push({name, url})` +2. Send ENTIRE array to POST endpoint: `body: JSON.stringify(this.servicesData)` +3. Flask `add_managed_service()` treats the array as a single object, calling `.get('name')` on a list → empty entry appended + +**Fix:** Send only the new item: +```javascript +body: JSON.stringify({ name, url }) +``` + +### 2.6 ⚠️ HIGH: admin.js saveApiKey() Sends Wrong Payload Shape +**File:** `js/admin.js:1289-1334` +**Impact:** API keys are never saved. The Flask endpoint returns 400 `{"error": "Invalid request: need group and data"}`. + +**Current JS payload:** `{ group: 'weather', weather_api_key: 'xxx' }` +**Expected by Flask:** `{ group: 'weather', data: { api_key: 'xxx' } }` + +**Fix:** Restructure the `saveApiKey()` method to wrap fields in `data`: +```javascript +async saveApiKey(group) { + const data = {}; + switch (group) { + case 'weather': + data.api_key = this.getVal('apiWeatherKey'); // also fix ID + break; + // etc. + } + const res = await fetch(this.API + '/apikeys', { + method: 'POST', + headers: this.authHeaders(), + body: JSON.stringify({ group, data }) + }); +} +``` + +### 2.7 ⚠️ HIGH: dashThreats Container Missing from admin.html +**File:** `admin.html` (dashboard section, ~line 126-167) +**Impact:** `admin.js:370` tries `document.getElementById('dashThreats')` — returns null, threats never display on dashboard. + +**Fix:** Add to dashboard section in admin.html: +```html +

▲ THREAT FEED

+
+``` + +### 2.8 ⚠️ HIGH: /api/services Causes Dashboard Load Cascade Failure +**File:** `js/admin.js:308-312` +**Impact:** `loadDashboard()` calls `Promise.all([stats, posts, tracks, services, threats])`. When `/api/services` hangs for 35+ seconds, the entire dashboard appears frozen even though other data loaded fine. + +**Fix:** Use `Promise.allSettled()` instead of `Promise.all()`, and handle each result independently: +```javascript +const results = await Promise.allSettled([ + fetch(this.API + '/stats', ...), + fetch(this.API + '/posts', ...), + // ... +]); +results.forEach((result, i) => { + if (result.status === 'fulfilled') { /* process */ } + else { console.warn('Dashboard fetch failed:', result.reason); } +}); +``` + +### 2.9 ⚠️ HIGH: nginx Warning — Duplicate MIME Type +**File:** `/etc/nginx/sites-enabled/cat.jaeswift.xyz:19` +**Impact:** Minor — duplicate `text/html` MIME type definition. No functional impact but generates warnings on every `nginx -t` and reload. + +**Fix:** Remove duplicate `text/html` from the types block in `cat.jaeswift.xyz` config. + +### 2.10 ⚠️ HIGH: nginx Warning — Protocol Options Redefined +**File:** `/etc/nginx/sites-enabled/jaeswift.xyz:26` +**Impact:** SSL protocol options redefined for `[::]:443`. First server block to define wins; subsequent definitions are ignored. + +**Fix:** Move SSL protocol directives to `http {}` block in `/etc/nginx/nginx.conf` or use `ssl_protocols` only once across all server blocks sharing the same listen address. + +### 2.11 ⚠️ HIGH: Flask Serves with Debug/Development Server +**File:** VPS systemd service runs `python3 /var/www/jaeswift-homepage/api/app.py` +**Impact:** Flask development server is single-threaded, not suitable for production. A single slow `/api/services` request blocks ALL other API requests. + +**Fix:** Use gunicorn or waitress: +```bash +# In systemd service: +ExecStart=/usr/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app +``` + +### 2.12 ⚠️ HIGH: Flask Makes Unverified HTTPS Requests +**File:** `api/app.py:195,263,267` — `verify=False` +**Impact:** Suppressed SSL certificate verification for requests to git.jaeswift.xyz and plex.jaeswift.xyz. Generates `InsecureRequestWarning` in logs. + +**Fix:** Either install proper CA certs or use `verify='/path/to/cert.pem'`. + +--- + +## 3. MEDIUM ISSUES + +### 3.1 🔶 MEDIUM: post.js Creates #bpmValue Dynamically +**File:** `js/post.js:153-154,171` +**Impact:** No current bug — post.js creates the element via innerHTML (line 153) then references it (line 171). Works because innerHTML is synchronous. However, static analysis flags this as a missing ID because post.html contains no such element. + +**Note:** Not a bug, but the pattern is fragile. If the innerHTML template changes, the getElementById will silently break. + +### 3.2 🔶 MEDIUM: admin.html Has No `` Tag for Login +**File:** `admin.html:16-31` +**Impact:** `admin.js:29` looks for `loginForm` by ID and finds null. The fallback (line 33-35) uses `loginBtn` click handler, which works. But Enter key on username field doesn't submit — only works on password field (line 38-41). + +**Fix:** Wrap login fields in `` with `onsubmit="return false;"`. + +### 3.3 🔶 MEDIUM: admin.js Notification Container Mismatch +**File:** `js/admin.js:259`, `admin.html:1045` +**Impact:** JS looks for `#notifications` (plural), HTML has `#notification` (singular). Fallback `|| document.body` means notifications append to body — they'll appear but may not be styled correctly. + +**Fix:** Change JS to use `notification` (matching HTML) or rename HTML ID. + +### 3.4 🔶 MEDIUM: blog.html Nav Links Point to Index Anchors +**File:** `blog.html` navbar +**Impact:** Links like `/#development`, `/#links`, `/#contact` navigate away from blog page back to index. This is intentional for cross-page navigation but there's no way back to blog from those sections. + +**Fix:** Consider adding a "BLOG" link in index.html navbar (currently only an anchor `#blog` section exists). + +### 3.5 🔶 MEDIUM: index.html Has `href="#"` Links +**File:** `index.html` — various social/placeholder links +**Impact:** Clicking scrolls to page top, poor UX. + +**Fix:** Replace `href="#"` with actual URLs or `javascript:void(0)`. + +### 3.6 🔶 MEDIUM: No favicon Referenced +**File:** All HTML files +**Impact:** Browser shows default icon, 404 in network tab for `/favicon.ico`. + +**Fix:** Add `` to all pages. + +### 3.7 🔶 MEDIUM: admin.css is 41KB / admin.js is 66KB +**Impact:** Large unminified files on every admin page load. + +**Fix:** Consider minification for production deployment. + +### 3.8 🔶 MEDIUM: No Cache-Busting for Static Assets +**File:** All HTML files reference `/js/main.js`, `/css/style.css` etc. with no query string version. + +**Impact:** Browser caching may serve stale JS/CSS after deployments. + +**Fix:** Add version query strings: `/js/main.js?v=20260401`. + +### 3.9 🔶 MEDIUM: app.py `/api/stats` Calls `shell()` for System Stats +**File:** `api/app.py:134` (via `shell()` function at line 54) +**Impact:** Executes shell commands (`uptime`, `df`, `free`, etc.) on every stats request. Not a security issue per se (no user input), but slow and could be cached. + +**Fix:** Cache stats with a TTL (e.g., 30 seconds). + +### 3.10 🔶 MEDIUM: No Rate Limiting on Login Endpoint +**File:** `api/app.py:62` +**Impact:** Brute force attacks possible against `/api/auth/login`. + +**Fix:** Add Flask-Limiter: `@limiter.limit("5/minute")`. + +### 3.11 🔶 MEDIUM: JWT Secret Hardcoded +**File:** `api/app.py` (JWT_SECRET referenced) +**Impact:** Secret is in source code committed to git. Anyone with repo access can forge tokens. + +**Fix:** Move to environment variable: `JWT_SECRET = os.environ.get('JWT_SECRET', 'fallback')`. + +### 3.12 🔶 MEDIUM: No Error Boundary in admin.js +**Impact:** Any unhandled JS error in one section breaks the entire admin panel. No global error handler. + +**Fix:** Add: +```javascript +window.addEventListener('unhandledrejection', (e) => { + console.error('Unhandled promise:', e.reason); + AdminApp.notify('An error occurred: ' + e.reason?.message, 'error'); +}); +``` + +### 3.13 🔶 MEDIUM: admin.html Editor Toolbar References HUD IDs +**File:** `admin.html:275-345` (Operator HUD section) +**Impact:** The range slider event listeners in `admin.js:85` reference `editEnergy/editMotivation/editFocus/editDifficulty` but HTML has `hudEnergy/hudMotivation/hudFocus/hudDifficulty`. The slider value display labels won't update live. + +**Fix:** Part of the broader ID mismatch (1.1.1). + +### 3.14 🔶 MEDIUM: blog.html and post.html Missing `` +**Impact:** Character encoding not explicitly set. Unicode characters (used extensively in cyberpunk UI) may render incorrectly in edge cases. + +**Fix:** Add `` to `` of both files. + +### 3.15 🔶 MEDIUM: Google Fonts Loaded Without `font-display: swap` +**File:** All HTML files +**Impact:** Text invisible during font load (FOIT — Flash of Invisible Text). + +**Fix:** Append `&display=swap` to Google Fonts URL (already present in some files, verify all). + +--- + +## 4. LOW ISSUES + +### 4.1 ℹ️ LOW: Root-Level JS Files Have Wrong Permissions (600) +**File:** `/var/www/jaeswift-homepage/*.js` +**Impact:** Files are owner-read-only. nginx runs as www-data and cannot read them. Not currently an issue because HTML references `/js/*.js` (the correct copies with 644), but if try_files falls through, these would be served as 403. + +**Fix:** `chmod 644 /var/www/jaeswift-homepage/*.js` or delete them (preferred — see 1.7). + +### 4.2 ℹ️ LOW: nginx Ghost Blog Catch-All Comment but No Block +**File:** `/etc/nginx/sites-enabled/jaeswift.xyz` — line with `# Ghost Blog (catch-all)` comment +**Impact:** Vestigial comment from previous Ghost setup. No functional issue. + +**Fix:** Remove the comment for cleanliness. + +### 4.3 ℹ️ LOW: admin.html Uses `javascript:void(0)` hrefs +**File:** `admin.html` sidebar links +**Impact:** Minor accessibility concern — screen readers announce these as links. + +**Fix:** Use `
- +
@@ -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');