jaeswift-website/AUDIT_REPORT.md
jae ccbd59fcd4 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
2026-04-01 00:54:20 +00:00

30 KiB
Raw Permalink Blame History

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 editXeditorX and editMood/Energy/etchudMood/Energy/etc, postIdeditorPostId.

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 dashXstatX, dashServicesGridservicesGrid. 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, themeGridthemeGridBg, valFontSizethemeFontSizeVal.

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: managedServicesListservicesList, serviceUrlserviceURL.

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: navItemsListnavList, navUrlnavURL.

admin.js uses admin.html has File:Line (JS)
managedLinksList linksList admin.js:1175
linkUrl linkURL admin.js:1201

Fix: managedLinksListlinksList, linkUrllinkURL.

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. notificationsnotification, trackUrltrackURL.


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.

  • 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:

// 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:

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:

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

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:

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:

@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:

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:

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:

<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:

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:

# 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,267verify=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.

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).

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:

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