# 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*
