Files

153 lines
8.8 KiB
Markdown

---
name: website-management
description: "Manage SpeedyFoxAI site across staging and production LXCs."
version: 1.0.0
tags: [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:
```bash
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:
```bash
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
```
2. Clean `/root/html/` — keep live site files + one backup dir:
```bash
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
```
3. Clean `/root/html/backup/` — keep only `20260326_201727/`, remove loose files and nested backups:
```bash
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
```
4. Remove cruft from `/root/` and `/home/n8n/`:
```bash
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
```
5. Re-deploy filezilla:
```bash
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:
```bash
# 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:
```bash
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:
```bash
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**:
```bash
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.