fix: resolve all critical audit issues
- Fix 60 DOM ID mismatches in admin.js (editor, dashboard, API keys, theme, services, navigation, links)
- Add 51 missing CSS classes to admin.css (sidebar, topbar, login, editor, tables, settings, backups, etc)
- Fix 5 undefined onclick methods in admin.html (saveContact, saveSEO, remove unused save buttons)
- Fix API payload mismatches: services/nav/links send single object, apikeys nested {group, data} format
- Replace Promise.all with Promise.allSettled in loadDashboard for resilient loading
- Fix /api/services timeout: ThreadPoolExecutor parallel checks + timeout=2s
- Add /etc/hosts entries on VPS for subdomain resolution from localhost
- Add JSON error handlers (400, 401, 404, 500) to Flask API
- Suppress InsecureRequestWarning in Flask
- Fix dashThreats container ID mismatch in admin.html
- Delete stale root-level JS files from VPS
This commit is contained in:
parent
cf737b3804
commit
ccbd59fcd4
5 changed files with 1371 additions and 178 deletions
658
AUDIT_REPORT.md
Normal file
658
AUDIT_REPORT.md
Normal file
|
|
@ -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 `<div id="dashThreats">` 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 `<form id="loginForm">` 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 `<title>401 Unauthorized</title>`.
|
||||
|
||||
**Evidence:**
|
||||
```
|
||||
curl http://127.0.0.1:5000/api/apikeys →
|
||||
<!doctype html><title>401 Unauthorized</title><h1>Unauthorized</h1><p>Missing token</p>
|
||||
```
|
||||
|
||||
**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
|
||||
<h3 class="section-subheading">▲ THREAT FEED</h3>
|
||||
<div id="dashThreats" class="threat-feed"></div>
|
||||
```
|
||||
|
||||
### 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 `<form>` 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 `<form id="loginForm">` 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 `<link rel="icon" href="/assets/favicon.ico">` 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 `<meta charset>`
|
||||
**Impact:** Character encoding not explicitly set. Unicode characters (used extensively in cyberpunk UI) may render incorrectly in edge cases.
|
||||
|
||||
**Fix:** Add `<meta charset="UTF-8">` to `<head>` 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 `<button>` elements instead of `<a>` tags for non-navigation actions.
|
||||
|
||||
### 4.4 ℹ️ LOW: No `<meta viewport>` in admin.html
|
||||
**Impact:** Admin panel may not render correctly on mobile without viewport meta tag.
|
||||
|
||||
**Fix:** Add `<meta name="viewport" content="width=device-width, initial-scale=1.0">` (check if already present).
|
||||
|
||||
### 4.5 ℹ️ LOW: InsecureRequestWarning Spam in Flask Logs
|
||||
**File:** Flask service journal
|
||||
**Impact:** Log noise from `verify=False` in requests to git/plex subdomains.
|
||||
|
||||
**Fix:** Add `import urllib3; urllib3.disable_warnings()` or fix the SSL verification.
|
||||
|
||||
### 4.6 ℹ️ LOW: admin.css Missing from Local Repo
|
||||
**File:** `/a0/usr/workdir/jaeswift-website/css/admin.css`
|
||||
**Impact:** The local copy exists but VPS copy has different content (VPS: 41,541 bytes). Ensure git is the source of truth.
|
||||
|
||||
**Fix:** Sync local and VPS copies via git.
|
||||
|
||||
### 4.7 ℹ️ LOW: posts.json Contains 9 Posts but No Pagination
|
||||
**Impact:** As posts grow, `/api/posts` returns all posts in a single response. Currently fine with 9 posts.
|
||||
|
||||
**Fix:** Add optional `?page=1&limit=10` query parameters to the posts endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 5. WHAT'S WORKING CORRECTLY ✅
|
||||
|
||||
| Component | Status |
|
||||
|---|---|
|
||||
| nginx → static HTML serving | ✅ All pages return 200 |
|
||||
| All 9 blog post slug URLs | ✅ `/blog/post/<slug>` resolves correctly |
|
||||
| Flask API on port 5000 | ✅ Active and running |
|
||||
| `/api/posts` GET | ✅ Returns 9 posts with valid JSON |
|
||||
| `/api/stats` GET | ✅ Returns server stats |
|
||||
| `/api/weather` GET | ✅ Returns weather data |
|
||||
| `/api/nowplaying` GET | ✅ Returns now playing info |
|
||||
| `/api/tracks` GET | ✅ Returns 35 tracks |
|
||||
| `/api/threats` GET | ✅ Returns CVE data |
|
||||
| `/api/settings` GET | ✅ Returns settings |
|
||||
| `/api/homepage` GET | ✅ Returns homepage config |
|
||||
| `/api/navigation` GET | ✅ Returns 3 nav items |
|
||||
| `/api/links` GET | ✅ Returns 2 links |
|
||||
| `/api/theme` GET | ✅ Returns theme config |
|
||||
| `/api/seo` GET | ✅ Returns SEO config |
|
||||
| `/api/services/managed` GET | ✅ Returns 3 managed services |
|
||||
| `/api/auth/login` POST | ✅ Returns JWT token |
|
||||
| Auth protection on endpoints | ✅ Returns 401 without token |
|
||||
| All 12 JSON data files | ✅ Valid JSON, correct types |
|
||||
| CSS files (style/blog/post) | ✅ All load with 200 |
|
||||
| JS files (main/blog/post) | ✅ All load with 200 |
|
||||
| main.js ↔ index.html IDs | ✅ All IDs match |
|
||||
| blog.js ↔ blog.html IDs | ✅ All IDs match |
|
||||
| SSL certificates | ✅ Valid Let's Encrypt |
|
||||
| nginx config syntax | ✅ Test passes (with warnings) |
|
||||
| Blog clean URLs | ✅ `/blog/post/.+` → post.html |
|
||||
| try_files fallback | ✅ `$uri $uri.html $uri/` |
|
||||
|
||||
---
|
||||
|
||||
## 6. PRIORITY FIX ORDER
|
||||
|
||||
1. **Fix 60 DOM ID mismatches in admin.js** (or admin.html) — this alone fixes ~80% of admin issues
|
||||
2. **Add 51 missing CSS classes to admin.css** — makes admin panel visually functional
|
||||
3. **Fix 5 undefined onclick methods** — either implement or rename to match existing
|
||||
4. **Fix API payload mismatches** — services/nav/links send single object, apikeys wrap in data
|
||||
5. **Fix /api/services timeout** — add /etc/hosts entries or parallelize with timeout
|
||||
6. **Delete stale root-level JS files on VPS**
|
||||
7. **Add CORS headers to Flask API**
|
||||
8. **Return JSON error responses from Flask (not HTML)**
|
||||
9. **Switch to gunicorn for production**
|
||||
10. **Add missing CSS classes for blog.html/post.html navbar**
|
||||
|
||||
---
|
||||
|
||||
*End of Audit Report*
|
||||
12
admin.html
12
admin.html
|
|
@ -162,7 +162,7 @@
|
|||
|
||||
<!-- Threat Feed -->
|
||||
<h3 class="section-subheading">⚠ THREAT FEED</h3>
|
||||
<div id="threatFeed" class="threat-feed">
|
||||
<div id="dashThreats" class="threat-feed">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -605,7 +605,7 @@
|
|||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn accent" onclick="AdminApp.saveServices()">⬡ SAVE SERVICES</button>
|
||||
<!-- Save button removed: add/delete persist via API -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -640,7 +640,7 @@
|
|||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn accent" onclick="AdminApp.saveNavigation()">⬡ SAVE NAVIGATION</button>
|
||||
<!-- Save button removed: add/delete persist via API -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -681,7 +681,7 @@
|
|||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn accent" onclick="AdminApp.saveLinks()">⬡ SAVE LINKS</button>
|
||||
<!-- Save button removed: add/delete persist via API -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -958,7 +958,7 @@
|
|||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn accent" onclick="AdminApp.saveSEO()">⬡ SAVE SEO</button>
|
||||
<button class="action-btn accent" onclick="AdminApp.saveSeo()">⬡ SAVE SEO</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -993,7 +993,7 @@
|
|||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn accent" onclick="AdminApp.saveContact()">⬡ SAVE CONTACT</button>
|
||||
<button class="action-btn accent" onclick="AdminApp.saveContactSettings()">⬡ SAVE CONTACT</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
39
api/app.py
39
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()
|
||||
|
|
|
|||
502
css/admin.css
502
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
338
js/admin.js
338
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 = `<span class="notification-icon">${type === 'success' ? '✓' : type === 'error' ? '✗' : 'ℹ'}</span><span>${msg}</span>`;
|
||||
|
|
@ -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 = {
|
|||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
} else {
|
||||
const grid = document.getElementById('servicesGrid');
|
||||
if (grid) grid.innerHTML = '<div style="color:#ff6600;padding:12px;">Services check timed out</div>';
|
||||
}
|
||||
} 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 = '<div class="threat-empty">No active threats detected ✓</div>';
|
||||
container.innerHTML = '<div class="threat-empty">No active threats detected \u2713</div>';
|
||||
} 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');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue