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-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:
- 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
- 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
- Clean
/root/html/backup/— keep only20260326_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
- 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
- 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.jsontracks/backup/paths underdownloadskey - 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-composevsdocker-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.mdfor 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 inparse_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.jsonexclusion 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 frommime.types, causing.htmlto serve asapplication/octet-stream(download prompt). Never usetypes {}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.jsonfiles 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 frommime.types, causing.htmlto serve asapplication/octet-stream(download prompt). Never usetypes {}in server blocks.