Files
hermes-skills/website-management/SKILL.md
T

8.8 KiB

name, description, version, tags
name description version tags
website-management Manage SpeedyFoxAI site across staging and production LXCs. 1.0.0
website
lxc
nginx
filezilla
sync
cleanup
music

website-management — SpeedyFoxAI Site Operations

Production LXC hosts the SpeedyFoxAI static site:

Role IP OS Notes
Production 10.0.0.39 Debian 13 Live site — all work goes here directly
Staging 10.0.0.17 Kali Linux Shut down 2026-08-11, kept as backup only

Production: n8n/passw0rd, nginx serving from /root/html/ on port 80, filezilla on 3025-3026.

SSH method: use sshpass for all remote commands — echo passw0rd | sudo -S fails with password prompts on these hosts. Pattern:

sshpass -p 'passw0rd' ssh -o StrictHostKeyChecking=no [email protected] '<command>'
sshpass -p 'passw0rd' scp -o StrictHostKeyChecking=no <src> [email protected]:<dst>

LXC Cleanup (run on both)

When cloning or resetting, strip to bare essentials:

  1. Remove stale Docker artifacts:
docker stop filezilla 2>/dev/null; docker rm filezilla 2>/dev/null
docker rm nginx 2>/dev/null  # stale container from original clone
docker rmi nginx:latest fauria/vsftpd:latest 2>/dev/null
docker volume prune -f; docker network prune -f
  1. Clean /root/html/ — keep live site files + one backup dir:
cd /root/html
rm -f .counter_data.txt counter.gif .counter_last_line .counter_total count.txt \
      index.html.bak.13022026 update_count_persistent.sh
  1. Clean /root/html/backup/ — keep only 20260326_201727/, remove loose files and nested backups:
cd /root/html/backup
rm -f clawdbot.json favicon.png "index (Copy 2).html" "index (Copy).html" \
      InstructionsGrok.txt jarvis-memory-blueprint.tar.gz \
      "LTX-2 FULL Guidelines.txt" openclaw.json yt_doc.md
rm -rf 20260326_201727/backup
  1. Remove cruft from /root/ and /home/n8n/:
rm -rf /root/backup /root/DocumentsUpload /root/docs /root/.projects /root/.projects-backup
rm -f /root/nginx-default.bak.13022026 /root/watcher.md
rm -rf ~/.projects ~/.main_projects ~/neuralstream-temp ~/filezilla-data
rm -f ~/site_log.md ~/ENDOFCARD ~/ENDOFFILE ~/EOF ~/HTML ~/SCRIPT_END
  1. Re-deploy filezilla:
docker run -d --name filezilla --restart unless-stopped \
  -p 3025:3000 -p 3026:3001 \
  -v /root/html:/config/data:rw \
  -e PUID=1000 -e PGID=1000 \
  lscr.io/linuxserver/filezilla:latest

Sync Staging → Production

After testing on .17, copy to .39:

# On .17: package the site
ssh [email protected] 'echo passw0rd | sudo -S tar czf /tmp/site.tar.gz -C /root/html .'

# Pull to local, push to .39
scp [email protected]:/tmp/site.tar.gz /tmp/
scp /tmp/site.tar.gz [email protected]:/tmp/

# On .39: extract
ssh [email protected] 'echo passw0rd | sudo -S tar xzf /tmp/site.tar.gz -C /root/html/ && echo passw0rd | sudo -S chown -R n8n:n8n /root/html/'

For music page specifically, package just the new files:

ssh [email protected] 'echo passw0rd | sudo -S tar czf /tmp/music_site.tar.gz -C /root/html music.html music/'

Music Page Architecture

See references/music-page.md for full architecture — library.json manifest, genre folder structure, animated visualizer, Tailwind theme integration.

Download counter: see references/download-counter.md — two-URL play vs download tracking via nginx access log parsing, zero backend, cron-driven counts.json.

Downloads Page Architecture

See references/downloads-page.md for full architecture — downloads.json manifest, type filter buttons, matching music page row style, download counter integration.

Quick reference:

  • Manifest: /root/html/backup/downloads.json{items: [{id, title, description, type, src}]}
  • Types: Image, JSON, TXT, MD, Skill — filter buttons auto-generated from manifest
  • Counter: same counts.json tracks /backup/ paths under downloads key
  • Styling: identical to music page — glass rows, type badge pill, centered count badge, download button on right

Music Management Quick Reference

Alphabetize after every change: tracks in library.json MUST be sorted by title (case-insensitive). Use:

sshpass -p 'passw0rd' ssh -o StrictHostKeyChecking=no [email protected] 'python3 -c "
import json; data = json.load(open(\"/root/html/music/audio/library.json\"))
data[\"tracks\"].sort(key=lambda t: t[\"title\"].lower())
json.dump(data, open(\"/root/html/music/audio/library.json\",\"w\"), indent=2)
"'

Move track between genres: update both genre and src fields in library.json, then re-sort.

Remove a genre: delete the folder under /root/html/music/audio/<genre>/, remove all tracks with that genre from library.json. The filter button disappears automatically — no HTML edit needed.

Validate all tracks playable:

sshpass -p 'passw0rd' ssh -o StrictHostKeyChecking=no [email protected] 'python3 -c "
import json, os
data = json.load(open(\"/root/html/music/audio/library.json\"))
missing = [t[\"title\"] for t in data[\"tracks\"] if not os.path.exists(\"/root/html\" + t[\"src\"])]
print(f\"All {len(data[\"tracks\"])} present\" if not missing else f\"MISSING: \" + \", \".join(missing))
"'

Sync missing files from NAS: source files on TrueNAS at smb://green.local/proxmoxbackup/SiteMusic/<genre>/. Download via smbclient, scp to .39, copy into genre folder. See references/music-page.md for full workflow.

Pitfalls

  • Kali vs Debian: .17 runs Kali (rolling), .39 runs Debian 13 (stable). Package names may differ (e.g., docker-compose vs docker-compose-plugin). Prefer Debian for production.
  • No swap on either LXC: 4.1GB RAM with no safety net. User explicitly declined swapfile — don't add without asking.
  • filezilla maps /root/html: any file dropped via filezilla lands in the nginx root. Permissions must stay n8n:n8n.
  • nginx runs as systemd service, NOT Docker: the nginx container is always stale — the live server is the native systemd unit.
  • index.html nav: when adding a new page tab, insert between Downloads and Contact in the nav <div class="hidden md:flex space-x-8">.
  • Count badge visibility: default gray-on-transparent styling (0.7rem, 500 weight, 0.7 opacity) is nearly invisible. Use indigo text (#6366f1), subtle indigo background pill, 600 weight, centered below title. See references/download-counter.md for the full CSS.
  • Sync from production to staging: when .17 is behind, pull from .39 via local machine as relay — ssh .39 tar → scp to local → scp to .17 → extract. Direct .39→.17 scp may not work if they can't reach each other.
  • 304 Not Modified not counted: counter only matches status 200 and 206. Browser-cached replays return 304 and are invisible. A user who plays a track, refreshes, and plays again from cache generates only one count. Fix: add '304' to the status check in parse_new_entries().
  • Query string pollution in counter: access log captures query strings (/backup/downloads.json?t=12345). Regex captures the full string including ?t=..., so .json exclusion checks fail. Fix: .split('?', 1)[0] on all captured paths before validation.
  • Leading slash mismatch in counter: some log paths have leading / (e.g. /electronic/Heavy-Weight.mp3). Frontend looks up without slash. Fix: .lstrip('/') on all captured paths. Clean existing data too.
  • nginx types{} block overrides ALL MIME types: adding types {} in a server/location context replaces inherited MIME types from mime.types, causing .html to serve as application/octet-stream (download prompt). Never use types {} in server blocks — add types to http-level config only.
  • Grok validation loop: dispatch to Grok for adversarial validation after building, fix findings, re-validate. Grok caught range-request inflation, log rotation loss, invalid HTML, path traversal, query string pollution, leading slash mismatch, and types{} MIME override — all real bugs.
  • Counter script must strip query strings: access log captures ?t=... cache busters. Use .split('?', 1)[0] on all captured paths before validation and key storage.
  • Counter script must strip leading slashes: some log paths have leading /. Use .lstrip('/') on all captured paths. Frontend lookup keys have no leading slash.
  • Backup path validation: is_valid_backup() must exclude .json files and paths ending in / from download counts. Apply AFTER stripping query strings.
  • Persistent accumulation over full-log re-parse: load existing counts.json, only process new entries via state file (inode+offset). Survives log rotation without re-parsing compressed logs.
  • nginx types{} block is dangerous: adding types {} in a server/location context replaces ALL inherited MIME types from mime.types, causing .html to serve as application/octet-stream (download prompt). Never use types {} in server blocks.