# JAESWIFT.XYZ — FULL SITE AUDIT REPORT **Date:** 2026-04-01 **Auditor:** Agent Zero Master Developer **Scope:** All HTML, CSS, JS, Flask API, nginx config, live endpoint testing --- ## EXECUTIVE SUMMARY The site's core infrastructure works: nginx serves pages (200 OK), Flask API runs on port 5000, all 9 blog post slugs resolve correctly, and static assets load. However, the **admin panel is critically broken** due to a systematic naming mismatch between `admin.js` and `admin.html` — 60 DOM element IDs referenced in JavaScript don't exist in the HTML. Additionally, 5 onclick handlers call undefined methods, 51 CSS classes are used in HTML but never defined, and several API payload shapes don't match between frontend and backend. ### Issue Count by Severity | Severity | Count | |----------|-------| | CRITICAL | 8 | | HIGH | 12 | | MEDIUM | 15 | | LOW | 7 | --- ## 1. CRITICAL ISSUES ### 1.1 ❌ CRITICAL: 60 DOM ID Mismatches — admin.js ↔ admin.html **Impact:** The entire admin panel is non-functional. Every section that reads/writes form fields silently fails because `getElementById()` returns `null`. #### 1.1.1 Editor Section (16 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `postId` | `editorPostId` | admin.js:451,524 | | `editTitle` | `editorTitle` | admin.js:66,452,525 | | `editSlug` | `editorSlug` | admin.js:453,526 | | `editDate` | `editorDate` | admin.js:460,463,527 | | `editTime` | `editorTime` | admin.js:461,464,528 | | `editTags` | `editorTags` | admin.js:467 | | `editExcerpt` | `editorExcerpt` | admin.js:468 | | `editContent` | `editorContent` | admin.js:72,469 | | `editMood` | `hudMood` | admin.js:472 | | `editEnergy` | `hudEnergy` | admin.js:85,476 | | `editMotivation` | `hudMotivation` | admin.js:85,477 | | `editFocus` | `hudFocus` | admin.js:85,478 | | `editDifficulty` | `hudDifficulty` | admin.js:85,479 | | `editCoffee` | `hudCoffee` | admin.js:490 | | `editBPM` | `hudBPM` | admin.js:493 | | `editThreat` | `hudThreat` | admin.js:496 | **Fix:** In `admin.js`, rename all `editX` → `editorX` and `editMood/Energy/etc` → `hudMood/Energy/etc`, `postId` → `editorPostId`. #### 1.1.2 Dashboard Section (8 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `dashCPU` | `statCPU` | admin.js:318 | | `dashMEM` | `statMEM` | admin.js:319 | | `dashDISK` | `statDISK` | admin.js:320 | | `dashPosts` | `statPosts` | admin.js:330 | | `dashWords` | `statWords` | admin.js:331 | | `dashTracks` | `statTracks` | admin.js:343 | | `dashServicesGrid` | `servicesGrid` | admin.js:351 | | `dashThreats` | *(not in HTML)* | admin.js:370 | **Fix:** In `admin.js`, rename `dashX` → `statX`, `dashServicesGrid` → `servicesGrid`. Add a threats container `
` to the dashboard section of `admin.html`, OR rename the JS reference. #### 1.1.3 API Keys Section (19 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `weatherApiKey` | `apiWeatherKey` | admin.js:1294 | | `spotifyClientId` | `apiSpotifyClientId` | admin.js:1297 | | `spotifyClientSecret` | `apiSpotifyClientSecret` | admin.js:1298 | | `spotifyRefreshToken` | `apiSpotifyRefreshToken` | admin.js:1299 | | `smtpHost` | `apiSmtpHost` | admin.js:1302 | | `smtpPort` | `apiSmtpPort` | admin.js:1303 | | `smtpUser` | `apiSmtpUser` | admin.js:1304 | | `smtpPass` | `apiSmtpPass` | admin.js:1305 | | `discordWebhook` | `apiDiscordWebhook` | admin.js:1308 | | `githubToken` | `apiGithubToken` | admin.js:1312 | | `customApi1Name` | `apiCustom1Name` | admin.js:1315 | | `customApi1Key` | `apiCustom1Key` | admin.js:1316 | | `customApi1Url` | `apiCustom1URL` | admin.js:1317 | | `customApi2Name` | `apiCustom2Name` | admin.js:1320 | | `customApi2Key` | `apiCustom2Key` | admin.js:1321 | | `customApi2Url` | `apiCustom2URL` | admin.js:1322 | | `customApi3Name` | `apiCustom3Name` | admin.js:1325 | | `customApi3Key` | `apiCustom3Key` | admin.js:1326 | | `customApi3Url` | `apiCustom3URL` | admin.js:1327 | **Fix:** In `admin.js`, rename all to match HTML IDs (add `api` prefix, use `URL` not `Url`). OR rename all HTML IDs to match JS convention. #### 1.1.4 Theme Section (7 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `themeAccent` | `themeAccentColor` | admin.js:1351+ | | `themeAccentHex` | `themeAccentColorHex` | admin.js:1351+ | | `themeBg` | `themeBgColor` | admin.js:1351+ | | `themeBgHex` | `themeBgColorHex` | admin.js:1351+ | | `themeText` | `themeTextColor` | admin.js:1351+ | | `themeTextHex` | `themeTextColorHex` | admin.js:1351+ | | `themeGrid` | `themeGridBg` | admin.js:1351+ | | `valFontSize` | `themeFontSizeVal` | admin.js:1351+ | **Fix:** In `admin.js`, add `Color` suffix to colour IDs, `themeGrid` → `themeGridBg`, `valFontSize` → `themeFontSizeVal`. #### 1.1.5 Services Section (2 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `managedServicesList` | `servicesList` | admin.js:1009 | | `serviceUrl` | `serviceURL` | admin.js:1034 | **Fix:** `managedServicesList` → `servicesList`, `serviceUrl` → `serviceURL`. #### 1.1.6 Navigation Section (2 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `navItemsList` | `navList` | admin.js:1089 | | `navUrl` | `navURL` | admin.js:1118 | **Fix:** `navItemsList` → `navList`, `navUrl` → `navURL`. #### 1.1.7 Links Section (2 mismatches) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `managedLinksList` | `linksList` | admin.js:1175 | | `linkUrl` | `linkURL` | admin.js:1201 | **Fix:** `managedLinksList` → `linksList`, `linkUrl` → `linkURL`. #### 1.1.8 Other Mismatches (4) | admin.js uses | admin.html has | File:Line (JS) | |---|---|---| | `loginForm` | *(no form element)* | admin.js:29 | | `notifications` | `notification` | admin.js:259 | | `trackUrl` | `trackURL` | admin.js:826+ | | *(none)* | `editorWordCount` (JS-created #bpmValue in post.js) | — | **Fix:** Either add `
` wrapper in HTML or change JS to use `loginBtn` click handler only. `notifications` → `notification`, `trackUrl` → `trackURL`. --- ### 1.2 ❌ CRITICAL: 5 Undefined onclick Methods **Impact:** Clicking Save buttons in 5 admin sections throws `AdminApp.X is not a function` error. | Method Called (admin.html) | Correct Method in admin.js | HTML Line | |---|---|---| | `AdminApp.saveServices()` | *(does not exist)* | admin.html:608 | | `AdminApp.saveNavigation()` | *(does not exist)* | admin.html:643 | | `AdminApp.saveLinks()` | *(does not exist)* | admin.html:684 | | `AdminApp.saveSEO()` | *(does not exist)* | admin.html:961 | | `AdminApp.saveContact()` | `AdminApp.saveContactSettings()` | admin.html:996 | **Fix for saveContact:** Change `admin.html:996` from `AdminApp.saveContact()` to `AdminApp.saveContactSettings()`. **Fix for saveServices/saveNavigation/saveLinks/saveSEO:** These methods need to be implemented in `admin.js`. Currently, the admin.js CRUD for services/nav/links works per-item (add/delete) but has no bulk "Save" method. Either: - (a) Remove the Save buttons from HTML (since add/delete already persist via API), OR - (b) Add PUT methods in admin.js that POST the full array For saveSEO, admin.js has `saveSeo()` (lowercase 'eo'). Change `admin.html:961` to `AdminApp.saveSeo()`. --- ### 1.3 ❌ CRITICAL: API Payload Shape Mismatches (Frontend ↔ Backend) **Impact:** POST requests from admin panel silently fail or corrupt data. #### 1.3.1 Services/Navigation/Links — Array vs Single Object - **admin.js `addService()`** (line 1047): Sends `JSON.stringify(this.servicesData)` — the **full array** - **app.py `add_managed_service()`** (line 342): Does `d = request.get_json(); svcs.append({name: d.get('name')})` — expects a **single object** - Result: Flask receives an array, calls `.get('name')` on it → returns `None`, appends `{name: '', url: ''}` empty entry Same pattern for: - `addNavItem()` (admin.js:1129) vs `add_navigation()` (app.py:374) - `addLink()` (admin.js:1213) vs `add_link()` (app.py:409) **Fix:** Change admin.js to send only the new item: ```javascript // In addService(): body: JSON.stringify({ name, url }) // NOT this.servicesData ``` #### 1.3.2 API Keys — Flat vs Nested Payload - **admin.js `saveApiKey()`** (line 1334): Sends `{ group: 'weather', weather_api_key: 'xxx' }` — flat object with group field - **app.py `save_apikeys()`** (line 474-476): Expects `{ group: 'weather', data: { api_key: 'xxx' } }` — nested with `data` dict - Result: `data = d.get('data', {})` returns empty dict → nothing saved **Fix:** Change admin.js `saveApiKey()` to wrap fields in a `data` object: ```javascript const payload = { group }; const data = {}; switch (group) { case 'weather': data.api_key = this.getVal('weatherApiKey'); break; // ... } payload.data = data; ``` --- ### 1.4 ❌ CRITICAL: /api/services Endpoint Hangs (Timeout) **File:** `api/app.py:181-200` **Impact:** Dashboard never finishes loading. Nginx logs show `upstream timed out (110: Connection timed out)`. The admin dashboard calls this at load time via `loadDashboard()`. **Root Cause:** The `services()` function makes synchronous HTTP requests to 7 external URLs (git.jaeswift.xyz, plex.jaeswift.xyz, etc.) from inside the Flask app. The VPS cannot resolve its own subdomains from localhost — `curl -sk https://git.jaeswift.xyz` returns code 000 (DNS/connection failure) with 3+ second timeout per service. Total: 7 × 5s timeout = 35s, exceeding nginx's default 60s proxy timeout. **Fix:** 1. Add `/etc/hosts` entries on VPS: `127.0.0.1 git.jaeswift.xyz plex.jaeswift.xyz archive.jaeswift.xyz` 2. OR reduce timeout in app.py: `req.get(s['url'], timeout=2, ...)` 3. OR use `concurrent.futures.ThreadPoolExecutor` to check all services in parallel 4. OR move service checks to a background task/cache --- ### 1.5 ❌ CRITICAL: /api/git-activity Endpoint Hangs **File:** `api/app.py:260-282` **Impact:** Same DNS issue as /api/services. Requests to `https://git.jaeswift.xyz/api/v1/users/jae/heatmap` fail from VPS localhost. **Fix:** Same as 1.4 — add hosts entries or reduce timeouts. --- ### 1.6 ❌ CRITICAL: 51 CSS Classes Missing from admin.css **Impact:** Major layout breakage in admin panel — sidebar, login screen, backups, settings, notifications, and many components render unstyled. **Missing classes referenced in admin.html but not defined in admin.css OR style.css:** | Class | Used In | Category | |---|---|---| | `.sidebar` | Layout wrapper | Layout | | `.sidebar-brand` | Brand logo area | Layout | | `.sidebar-close` | Mobile close btn | Layout | | `.sidebar-divider` | Section dividers | Layout | | `.sidebar-header` | Header area | Layout | | `.sidebar-nav` | Nav container | Layout | | `.sidebar-section-label` | Group labels | Layout | | `.sidebar-version` | Version footer | Layout | | `.topbar` | Top bar | Layout | | `.topbar-title` | Title text | Layout | | `.topbar-logout` | Logout btn | Layout | | `.login-screen` | Login wrapper | Auth | | `.login-form` | Form container | Auth | | `.login-logo` | Logo area | Auth | | `.notification` | Toast container | UI | | `.operator-hud` | Editor HUD | Editor | | `.hud-grid` | HUD metrics grid | Editor | | `.hud-range` | Range sliders | Editor | | `.hud-val` | Value labels | Editor | | `.editor-split` | Split view | Editor | | `.editor-pane` | Editor pane | Editor | | `.preview-pane` | Preview pane | Editor | | `.editor-meta-bar` | Meta info bar | Editor | | `.editor-textarea-sm` | Small textareas | Editor | | `.section-toolbar` | Toolbar | Editor | | `.toolbar-btn` | Toolbar buttons | Editor | | `.toolbar-sep` | Toolbar separator | Editor | | `.admin-table` | Posts table | Posts | | `.table-wrap` | Table wrapper | Posts | | `.tracks-add-form` | Add track form | Tracks | | `.tracks-list` | Tracks container | Tracks | | `.settings-grid` | Settings layout | Settings | | `.setting-item` | Setting row | Settings | | `.toggle-slider` | Toggle control | Settings | | `.homepage-section-name` | Section label | Homepage | | `.homepage-section-controls` | Controls | Homepage | | `.services-manage-list` | Services list | Services | | `.nav-manage-list` | Nav items list | Navigation | | `.links-manage-list` | Links list | Links | | `.apikey-group` | Key group card | API Keys | | `.color-input` | Colour picker | Theme | | `.color-hex` | Hex input | Theme | | `.sm` | Small variant | Utility | | `.lg` | Large variant | Utility | | `.backup-card-icon` | Card icon | Backups | | `.backup-card-title` | Card title | Backups | | `.backup-card-desc` | Card description | Backups | | `.backup-desc` | Description text | Backups | | `.backup-export` | Export section | Backups | | `.backup-status` | Status area | Backups | | `.threat-feed` | Threats container | Dashboard | **Fix:** These classes must be defined in `admin.css`. Many were apparently planned (the HTML uses them consistently) but never written. --- ### 1.7 ❌ CRITICAL: Stale Root-Level JS Files on VPS **Impact:** Potential confusion; the root-level `/var/www/jaeswift-homepage/admin.js` (66,567 bytes) is an OLDER version than `/var/www/jaeswift-homepage/js/admin.js` (66,766 bytes). The diff shows the root copy is missing the `showSection()` normalization fix. **Diff:** ```diff # Root admin.js is MISSING these 3 lines present in js/admin.js: + // Normalize: ensure name has section- prefix + if (!name.startsWith('section-')) name = 'section-' + name; # Root admin.js has outdated sidebar active-link selector: - const activeLink = document.querySelector(`[onclick*="'${name}'"]`); # js/admin.js has fixed version: + const shortName = name.replace('section-', ''); + const activeLink = document.querySelector(`.sidebar-link[data-section="${shortName}"]`); ``` **Affected root files (permissions also wrong — 600 vs 644):** | File | Root Size | js/ Size | Match? | |---|---|---|---| | admin.js | 66,567 (perms: 600) | 66,766 (perms: 644) | ❌ DIFFERENT | | blog.js | 7,325 (perms: 600) | 7,325 (perms: 644) | ✅ Same | | main.js | 36,092 (perms: 600) | 36,092 (perms: 644) | ✅ Same | | post.js | 13,473 (perms: 600) | 13,473 (perms: 644) | ✅ Same | **Fix:** Delete the stale root-level JS files: ```bash rm /var/www/jaeswift-homepage/admin.js /var/www/jaeswift-homepage/blog.js \ /var/www/jaeswift-homepage/main.js /var/www/jaeswift-homepage/post.js ``` --- ### 1.8 ❌ CRITICAL: showSection() Broken in Root admin.js **File:** Root `/var/www/jaeswift-homepage/admin.js` (stale copy) **Impact:** If somehow the root copy were served (e.g., nginx `try_files` fallback), the sidebar section switching would not work because the old code lacks the `section-` prefix normalization. Note: Currently nginx serves from `/js/admin.js` (the correct file), so this is only a risk if the stale file is served by accident. Still, it should be cleaned up. **Fix:** Delete root-level JS files (see 1.7). --- ## 2. HIGH ISSUES ### 2.1 ⚠️ HIGH: blog.html and post.html — 4 CSS Classes Undefined **Impact:** Navbar, clock, and signal animation don't render correctly on blog pages. | Class | File | Not in | |---|---|---| | `.navbar` | blog.html, post.html | blog.css, style.css | | `.nav-clock` | blog.html, post.html | blog.css, style.css | | `.signal-flicker` | blog.html, post.html | blog.css, style.css | | `.blog-header-content` | blog.html only | blog.css, style.css | **Fix:** Define these classes in `blog.css` or `style.css`. The index.html uses different class names for the navbar (likely defined in style.css under different names). ### 2.2 ⚠️ HIGH: No CORS Headers on API **Impact:** If any external/subdomain frontend tries to call the API, requests will be blocked. **Test result:** `curl -sI /api/posts | grep access-control` returned nothing. **Fix:** Add Flask-CORS or manual headers: ```python from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "https://jaeswift.xyz"}}) ``` ### 2.3 ⚠️ HIGH: Auth Endpoints Return HTML Error Pages **File:** `api/app.py:41-52` (`require_auth` decorator) **Impact:** Admin.js expects JSON responses but gets HTML `401 Unauthorized`. **Evidence:** ``` curl http://127.0.0.1:5000/api/apikeys → 401 Unauthorized

Unauthorized

Missing token

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

▲ THREAT FEED

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