feat: revamp Jefflix buttons, simplify onboarding, add backlog tasks

Replace Movies/Shows/Music/Live Sports buttons with Request/Watch/Upload
flow. Update request-access page with clear requests.jefflix.lol and
movies.jefflix.lol instructions plus install-as-app tips. Clean up
README, remove @vercel/analytics, add backlog tasks and vpn-setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 22:39:48 -07:00
parent b43a72a080
commit 2c8614e01e
15 changed files with 649 additions and 90 deletions

View File

@ -1,30 +1 @@
# Jefflix website
*Automatically synced with your [v0.app](https://v0.app) deployments*
[![Deployed on Vercel](https://img.shields.io/badge/Deployed%20on-Vercel-black?style=for-the-badge&logo=vercel)](https://vercel.com/jeff-emmetts-projects/v0-jefflix-website)
[![Built with v0](https://img.shields.io/badge/Built%20with-v0.app-black?style=for-the-badge)](https://v0.app/chat/rSGm1BAgi15)
## Overview
This repository will stay in sync with your deployed chats on [v0.app](https://v0.app).
Any changes you make to your deployed app will be automatically pushed to this repository from [v0.app](https://v0.app).
## Deployment
Your project is live at:
**[https://vercel.com/jeff-emmetts-projects/v0-jefflix-website](https://vercel.com/jeff-emmetts-projects/v0-jefflix-website)**
## Build your app
Continue building your app on:
**[https://v0.app/chat/rSGm1BAgi15](https://v0.app/chat/rSGm1BAgi15)**
## How It Works
1. Create and modify your project using [v0.app](https://v0.app)
2. Deploy your chats from the v0 interface
3. Changes are automatically pushed to this repository
4. Vercel deploys the latest version from this repository

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Film, Music, Server, Users, Heart, Rocket, Tv, Home, FolderGitIcon as SolidarityFistIcon, HandHeart, Landmark, Palette, Radio } from "lucide-react"
import { Film, Music, Server, Users, Heart, Rocket, Tv, Home, FolderGitIcon as SolidarityFistIcon, HandHeart, Landmark, Palette, Radio, ListPlus, Play, Upload } from "lucide-react"
import { JefflixLogo } from "@/components/jefflix-logo"
export default function JefflixPage() {
@ -48,54 +48,36 @@ export default function JefflixPage() {
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-red-600 hover:bg-red-700 text-white"
className="text-lg px-8 py-6 font-bold bg-blue-600 hover:bg-blue-700 text-white"
variant="default"
>
<a href="https://movies.jefflix.lol">
<Film className="mr-2 h-5 w-5" />
Movies
<a href="https://requests.jefflix.lol">
<ListPlus className="mr-2 h-5 w-5" />
Request a Show or Movie
</a>
</Button>
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-blue-600 hover:bg-blue-700 text-white"
className="text-lg px-8 py-6 font-bold bg-red-600 hover:bg-red-700 text-white"
variant="default"
>
<a href="https://movies.jefflix.lol">
<Tv className="mr-2 h-5 w-5" />
Shows
<Play className="mr-2 h-5 w-5" />
Watch a Show or Movie
</a>
</Button>
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-green-600 hover:bg-green-700 text-white"
variant="outline"
variant="default"
>
<a href="https://music.jefflix.lol">
<Music className="mr-2 h-5 w-5" />
Music
<a href="https://upload.jefflix.lol">
<Upload className="mr-2 h-5 w-5" />
Upload Shows or Movies
</a>
</Button>
<div className="relative group">
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-orange-600 hover:bg-orange-700 text-white"
variant="default"
>
<a href="https://movies.jefflix.lol/web/index.html#!/livetv.html">
<Radio className="mr-2 h-5 w-5" />
Live Sports
</a>
</Button>
<a href="/request-access" className="absolute -top-2 -right-2">
<Badge className="bg-yellow-500 hover:bg-yellow-400 text-black text-xs px-2 py-0.5 font-bold cursor-pointer transition-colors">
Request Access
</Badge>
</a>
</div>
</div>
</div>
</div>
@ -222,20 +204,20 @@ export default function JefflixPage() {
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-6">
<Button asChild size="lg" className="text-lg px-8 py-6 font-bold bg-red-600 hover:bg-red-700 text-white">
<a href="https://movies.jefflix.lol">
<Film className="mr-2 h-5 w-5" />
Movies
<Button asChild size="lg" className="text-lg px-8 py-6 font-bold bg-blue-600 hover:bg-blue-700 text-white">
<a href="https://requests.jefflix.lol">
<ListPlus className="mr-2 h-5 w-5" />
Request a Show or Movie
</a>
</Button>
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-blue-600 hover:bg-blue-700 text-white"
className="text-lg px-8 py-6 font-bold bg-red-600 hover:bg-red-700 text-white"
>
<a href="https://movies.jefflix.lol">
<Tv className="mr-2 h-5 w-5" />
Shows
<Play className="mr-2 h-5 w-5" />
Watch a Show or Movie
</a>
</Button>
<Button
@ -243,28 +225,11 @@ export default function JefflixPage() {
size="lg"
className="text-lg px-8 py-6 font-bold bg-green-600 hover:bg-green-700 text-white"
>
<a href="https://music.jefflix.lol">
<Music className="mr-2 h-5 w-5" />
Music
<a href="https://upload.jefflix.lol">
<Upload className="mr-2 h-5 w-5" />
Upload Shows or Movies
</a>
</Button>
<div className="relative group">
<Button
asChild
size="lg"
className="text-lg px-8 py-6 font-bold bg-orange-600 hover:bg-orange-700 text-white"
>
<a href="https://movies.jefflix.lol/web/index.html#!/livetv.html">
<Radio className="mr-2 h-5 w-5" />
Live Sports
</a>
</Button>
<a href="/request-access" className="absolute -top-2 -right-2">
<Badge className="bg-yellow-500 hover:bg-yellow-400 text-black text-xs px-2 py-0.5 font-bold cursor-pointer transition-colors">
Request Access
</Badge>
</a>
</div>
</div>
<p className="text-sm text-muted-foreground pt-4">
Or learn how to set up your own Jellyfin server and join the movement

View File

@ -57,6 +57,14 @@ export default function RequestAccessPage() {
<p className="text-sm text-muted-foreground">
This usually happens within 24-48 hours.
</p>
<div className="text-left bg-muted/50 rounded-lg p-6 mt-4 space-y-3">
<h3 className="font-bold text-center">Once you're approved:</h3>
<ul className="text-sm text-muted-foreground space-y-2 list-disc list-inside">
<li>Request movies & shows at <a href="https://requests.jefflix.lol" className="text-blue-600 hover:underline font-medium">requests.jefflix.lol</a></li>
<li>Watch them at <a href="https://movies.jefflix.lol" className="text-red-600 hover:underline font-medium">movies.jefflix.lol</a></li>
<li>Both sites can be installed as apps on your phone's home screen, or use the Jellyfin app on a smart TV</li>
</ul>
</div>
<Link href="/">
<Button variant="outline" className="mt-4">
<ArrowLeft className="mr-2 h-4 w-4" />
@ -159,14 +167,17 @@ export default function RequestAccessPage() {
</Button>
</form>
<div className="mt-8 p-6 bg-muted/50 rounded-lg">
<div className="mt-8 p-6 bg-muted/50 rounded-lg space-y-4">
<h3 className="font-bold mb-2">What happens next?</h3>
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
<li>Your request is sent to the admin for review</li>
<li>Once approved, you'll get an email with your login details</li>
<li>Log in at movies.jefflix.lol to access all content</li>
<li>Live Sports requires an active Sportsnet subscription</li>
<li>Request movies & shows at <a href="https://requests.jefflix.lol" className="text-blue-600 hover:underline font-medium">requests.jefflix.lol</a></li>
<li>Watch them at <a href="https://movies.jefflix.lol" className="text-red-600 hover:underline font-medium">movies.jefflix.lol</a></li>
</ol>
<p className="text-xs text-muted-foreground border-t border-border pt-3">
Both sites can be installed as apps on your phone's home screen. On a smart TV, use the Jellyfin app and connect to movies.jefflix.lol.
</p>
</div>
</div>
</div>

View File

@ -13,3 +13,4 @@ auto_commit: false
bypass_git_hooks: false
check_active_branches: true
active_branch_days: 30
task_prefix: "task"

View File

@ -0,0 +1,47 @@
---
id: TASK-3
title: Set up Navidrome mobile access (Android + iOS)
status: To Do
assignee: []
created_date: '2026-02-24 07:13'
labels:
- soulsync
- mobile
- navidrome
dependencies: []
references:
- 'https://soulsync.jefflix.lol'
- 'https://music.jefflix.lol'
- 'https://soulseek.jefflix.lol'
- /home/jeffe/Github/jefflix-website/soulsync-docker-compose.yml
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Configure Navidrome (music.jefflix.lol) for mobile streaming on both Android and Apple devices using Subsonic-compatible apps.
Navidrome exposes the Subsonic API, so no custom app development is needed - just configure a native mobile client on each platform.
**Recommended Apps:**
- **Android:** Symfonium (paid, best UX), Subtracks (free/open-source), DSub, Ultrasonic
- **iOS/Apple:** play:Sub, Amperfy (free/open-source), SubStreamer, iSub
**Server URL:** https://music.jefflix.lol
**Prerequisites:**
- Navidrome must be accessible externally (verify Cloudflare tunnel routing)
- User account(s) created in Navidrome
- Subsonic API enabled in Navidrome settings (usually on by default)
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Navidrome is accessible externally at music.jefflix.lol
- [ ] #2 Subsonic API endpoint responds (music.jefflix.lol/rest/ping)
- [ ] #3 Android app installed and streaming music successfully
- [ ] #4 iOS app installed and streaming music successfully
- [ ] #5 Offline download/caching tested on at least one platform
- [ ] #6 Document the setup (app name, settings) for future reference
<!-- AC:END -->

View File

@ -0,0 +1,40 @@
---
id: TASK-4
title: Verify SoulSync playlist sync pipeline end-to-end
status: To Do
assignee: []
created_date: '2026-02-24 07:13'
labels:
- soulsync
- maintenance
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Ensure the full SoulSync pipeline is working: Spotify playlist → SoulSync orchestration → Soulseek download → Navidrome library update.
Spotify API was configured (task-1), but we should verify the full flow works reliably before relying on mobile access.
**Services to check:**
- SoulSync web UI: https://soulsync.jefflix.lol
- slskd (Soulseek): https://soulseek.jefflix.lol
- Navidrome: https://music.jefflix.lol
**Key checks:**
- Are Spotify playlists syncing to SoulSync?
- Are downloads completing via Soulseek?
- Is Navidrome picking up new files from the music directory?
- Are there any stale/failed downloads to clean up?
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 SoulSync shows Spotify playlists synced
- [ ] #2 At least one playlist successfully downloads tracks via Soulseek
- [ ] #3 Downloaded tracks appear in Navidrome library
- [ ] #4 No stuck/failed jobs in the queue
- [ ] #5 slskd Soulseek connection is healthy and sharing
<!-- AC:END -->

View File

@ -0,0 +1,44 @@
---
id: TASK-5
title: Audit games platform deployment on Netcup
status: To Do
assignee: []
created_date: '2026-02-24 07:13'
labels:
- games
- infrastructure
- audit
dependencies: []
references:
- 'https://games.jeffemmett.com'
- /home/jeffe/Github/games-platform/docker-compose.yml
- /home/jeffe/Github/games-platform/DEPLOYMENT.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Check the current state of the games platform deployment at games.jeffemmett.com on Netcup RS 8000.
**What to check:**
- Are all Docker containers running? (postgres, redis, backend, worker, frontend, nginx)
- Is the site accessible at https://games.jeffemmett.com?
- What games (if any) are currently in the library?
- Check /data/games/ directories for existing ROMs
- Review database for any game entries
- Check if the auto-deploy webhook is working
- Verify EmulatorJS loads correctly in browser
**Location on server:** /opt/apps/games-platform
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 All 6 Docker containers verified running (or restarted)
- [ ] #2 games.jeffemmett.com loads in browser
- [ ] #3 Inventory of existing games documented
- [ ] #4 Database health confirmed
- [ ] #5 Gitea webhook auto-deploy verified
- [ ] #6 EmulatorJS emulator loads on a game page
<!-- AC:END -->

View File

@ -0,0 +1,45 @@
---
id: TASK-6
title: Catalog desired retro games for games platform
status: To Do
assignee: []
created_date: '2026-02-24 07:14'
labels:
- games
- content
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Create a wishlist of old/retro games to add to the games platform. Go through memories of classic games across all supported platforms and build a catalog of what to source.
**Supported platforms:**
- PlayStation 1 (.iso, .bin, .cue, .pbp)
- Nintendo 64 (.z64, .n64, .v64)
- Super Nintendo (.smc, .sfc)
- Game Boy Advance (.gba)
- Game Boy Color (.gbc, .gb)
- NES (.nes)
- Sega Genesis (.md, .bin, .gen)
- Sega Dreamcast (.cdi, .gdi, .chd)
- PSP (.iso, .cso, .pbp)
**Process:**
1. List out all desired games by platform
2. Note which ones are personal favorites / must-haves
3. Research ROM availability and file sizes
4. Prioritize which to add first
5. Source ROM files (user handles this manually)
6. Upload via the games platform API or direct file copy
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Game wishlist created with at least 10 games across multiple platforms
- [ ] #2 Games prioritized by platform and personal preference
- [ ] #3 File size estimates noted for storage planning
- [ ] #4 Top 5 must-have games identified for first batch
<!-- AC:END -->

View File

@ -0,0 +1,43 @@
---
id: TASK-7
title: Add ROM files and populate games library
status: To Do
assignee: []
created_date: '2026-02-24 07:14'
labels:
- games
- content
dependencies:
- TASK-5
- TASK-4
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Once games are sourced (from task: Catalog desired retro games), upload ROMs to the games platform and register them in the database.
**Methods to add games:**
1. **Direct file copy:** SCP ROMs to /data/games/{platform}/ on Netcup, then register via API
2. **Upload API:** POST to /api/upload with ROM file
3. **Web UI:** Upload through the games platform interface
**For each game added:**
- Copy ROM to correct platform directory
- Add cover art if available
- Register in database with metadata (title, year, description, genre)
- Test that it loads in EmulatorJS
- Verify save states work
**Storage location on Netcup:** /data/games/{ps1,n64,snes,gba,gbc,nes,genesis,dreamcast,psp}/
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 At least 5 games uploaded and playable
- [ ] #2 Each game has cover art and metadata
- [ ] #3 EmulatorJS loads and runs each game
- [ ] #4 Save states work for at least one game per platform
- [ ] #5 Games appear correctly in the library browse page
<!-- AC:END -->

View File

@ -37,7 +37,6 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "latest",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

37
vpn-setup/README.md Normal file
View File

@ -0,0 +1,37 @@
# Jefflix VPN Setup — Headscale + Tailscale
Protects all `*.jefflix.lol` services behind the existing Headscale VPN at `vpn.jeffemmett.com`.
## How It Works
```
Before (public):
Browser → Cloudflare → Tunnel → Traefik → Jellyfin/etc
After (VPN-only):
Browser → Tailscale (WireGuard) → Traefik → Jellyfin/etc
(Only works if connected to the tailnet)
```
Traefik still routes by Host header — the only change is how traffic reaches it.
## Quick Start
SSH into the server and follow the phases in order:
```bash
ssh netcup
```
Then run `setup.sh` (or follow the manual steps below).
## Files
| File | Purpose |
|------|---------|
| `setup.sh` | Full setup script (run on Netcup) |
| `coredns/Corefile` | CoreDNS config — resolves *.jefflix.lol to Tailscale IP |
| `coredns/docker-compose.yml` | CoreDNS container definition |
| `headscale-config-patch.yaml` | Split DNS addition for Headscale config |
| `cloudflared-config-clean.yml` | Cloudflare tunnel config with jefflix entries removed |
| `rollback.sh` | Emergency rollback script |

99
vpn-setup/cutover.sh Executable file
View File

@ -0,0 +1,99 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Jefflix VPN Cutover Script
# Removes public access to *.jefflix.lol
# Run ONLY after setup.sh and testing VPN access works
# ============================================================
echo "========================================"
echo " Jefflix Public Access Cutover"
echo "========================================"
echo ""
# Pre-flight check: verify VPN access works
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "")
if [ -z "$TAILSCALE_IP" ]; then
echo "ERROR: Tailscale not running on this server. Run setup.sh first."
exit 1
fi
echo "Server Tailscale IP: $TAILSCALE_IP"
echo ""
# Check DNS works
DIG_RESULT=$(dig +short @${TAILSCALE_IP} movies.jefflix.lol 2>/dev/null || echo "FAILED")
if [ "$DIG_RESULT" != "$TAILSCALE_IP" ]; then
echo "ERROR: CoreDNS not resolving correctly. Got: $DIG_RESULT (expected $TAILSCALE_IP)"
echo "Fix CoreDNS before proceeding."
exit 1
fi
echo "✓ CoreDNS resolving correctly"
echo ""
echo "This will REMOVE public access to all *.jefflix.lol services."
echo "Users will need Tailscale connected to vpn.jeffemmett.com to access Jefflix."
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
# --- Remove jefflix entries from Cloudflare tunnel ---
echo ""
echo "[Cutover] Removing *.jefflix.lol from Cloudflare tunnel..."
TUNNEL_CONFIG="/root/cloudflared/config.yml"
# Backup current config (timestamped)
cp "$TUNNEL_CONFIG" "${TUNNEL_CONFIG}.pre-cutover-$(date +%Y%m%d-%H%M%S)"
# Remove all jefflix.lol hostname entries (hostname + service lines)
# This removes the "- hostname: *.jefflix.lol" and its " service:" line
python3 -c "
import yaml, sys
with open('$TUNNEL_CONFIG', 'r') as f:
config = yaml.safe_load(f)
original_count = len(config.get('ingress', []))
# Filter out jefflix.lol entries
config['ingress'] = [
entry for entry in config.get('ingress', [])
if not (isinstance(entry.get('hostname', ''), str) and 'jefflix.lol' in entry.get('hostname', ''))
]
removed = original_count - len(config['ingress'])
with open('$TUNNEL_CONFIG', 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print(f' Removed {removed} jefflix.lol entries from tunnel config')
"
# Restart cloudflared
echo " Restarting cloudflared..."
docker restart cloudflared
echo " ✓ Cloudflared restarted"
# Wait and verify
sleep 5
echo ""
echo "========================================"
echo " Cutover Complete"
echo "========================================"
echo ""
echo "Public access to *.jefflix.lol is now REMOVED."
echo ""
echo "Verify from a non-VPN device:"
echo " curl -I https://movies.jefflix.lol (should fail/404)"
echo ""
echo "Verify from a VPN device:"
echo " curl http://movies.jefflix.lol (should work)"
echo ""
echo "To ROLLBACK: run rollback.sh"

66
vpn-setup/onboard-user.sh Executable file
View File

@ -0,0 +1,66 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Generate a Jefflix VPN invite for a new user
# Usage: ./onboard-user.sh [username]
# ============================================================
USERNAME="${1:-}"
if [ -z "$USERNAME" ]; then
echo "Usage: ./onboard-user.sh <username>"
echo " Creates a pre-auth key for the user and prints setup instructions."
exit 1
fi
# Generate a single-use pre-auth key (7 day expiry)
echo "Generating pre-auth key for user: $USERNAME"
PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create \
--user jefflix \
--reusable=false \
--expiration 168h 2>&1 | tail -1)
echo ""
echo "============================================================"
echo " Send the following to $USERNAME:"
echo "============================================================"
echo ""
cat <<MSG
Hey! Here's how to connect to Jefflix:
1. Install Tailscale on your device:
- Windows/Mac/Linux: https://tailscale.com/download
- iOS: Search "Tailscale" in the App Store
- Android: Search "Tailscale" in Google Play
2. Connect to the Jefflix VPN:
Desktop (open a terminal and run):
tailscale up --login-server=https://vpn.jeffemmett.com --authkey=$PREAUTH_KEY
iOS: Settings → tap account → "..." → Use custom coordination server
Server: https://vpn.jeffemmett.com
Then log in with this key: $PREAUTH_KEY
Android: Settings → tap account → "..." → Use alternate server
Server: https://vpn.jeffemmett.com
Then log in with this key: $PREAUTH_KEY
3. Once connected, open your browser and go to:
- http://movies.jefflix.lol (Watch movies & shows)
- http://requests.jefflix.lol (Request new content)
- http://upload.jefflix.lol (Upload your own content)
- http://music.jefflix.lol (Listen to music)
Tailscale runs in the background — you only need to set it up once!
NOTE: This invite key expires in 7 days. Let me know if you need a new one.
MSG
echo ""
echo "============================================================"
echo " Key: $PREAUTH_KEY"
echo " Expires: 7 days"
echo " User: jefflix"
echo "============================================================"

40
vpn-setup/rollback.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Jefflix VPN Rollback Script
# Restores public access to *.jefflix.lol
# ============================================================
echo "========================================"
echo " Jefflix VPN Rollback"
echo "========================================"
echo ""
# Restore Cloudflare tunnel config
if [ -f /root/cloudflared/config.yml.backup-jefflix-vpn ]; then
cp /root/cloudflared/config.yml.backup-jefflix-vpn /root/cloudflared/config.yml
docker restart cloudflared
echo "✓ Cloudflare tunnel config restored and restarted"
else
echo "⚠ No backup found at /root/cloudflared/config.yml.backup-jefflix-vpn"
fi
# Restore Headscale config
if [ -f /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn ]; then
cp /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn /opt/apps/headscale-deploy/config/config.yaml
cd /opt/apps/headscale-deploy && docker compose restart headscale
echo "✓ Headscale config restored and restarted"
else
echo "⚠ No Headscale backup found"
fi
# Stop CoreDNS (optional — it doesn't hurt to leave it running)
if docker ps --format '{{.Names}}' | grep -q jefflix-dns; then
cd /opt/apps/jefflix-dns && docker compose down
echo "✓ CoreDNS stopped"
fi
echo ""
echo "Rollback complete. Public access to *.jefflix.lol should be restored."
echo "Verify: curl -I https://movies.jefflix.lol"

151
vpn-setup/setup.sh Executable file
View File

@ -0,0 +1,151 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Jefflix VPN Setup Script
# Run this on the Netcup RS 8000 server (ssh netcup)
# ============================================================
echo "========================================"
echo " Jefflix VPN Setup (Headscale/Tailscale)"
echo "========================================"
echo ""
# --- Phase 1: Backups ---
echo "[Phase 1] Creating backups..."
cp /root/cloudflared/config.yml /root/cloudflared/config.yml.backup-jefflix-vpn
cp /opt/apps/headscale-deploy/config/config.yaml /opt/apps/headscale-deploy/config/config.yaml.backup-jefflix-vpn
cp -r /root/traefik/config/ /root/traefik/config-backup-jefflix-vpn/ 2>/dev/null || true
echo " ✓ Backups created"
# --- Phase 1b: Create Headscale users ---
echo ""
echo "[Phase 1b] Creating Headscale users..."
docker exec headscale headscale users create server 2>/dev/null || echo " (user 'server' already exists)"
docker exec headscale headscale users create jefflix 2>/dev/null || echo " (user 'jefflix' already exists)"
echo " ✓ Users ready"
# --- Phase 2: Install Tailscale ---
echo ""
echo "[Phase 2] Installing Tailscale..."
if command -v tailscale &>/dev/null; then
echo " Tailscale already installed: $(tailscale version)"
else
curl -fsSL https://tailscale.com/install.sh | sh
echo " ✓ Tailscale installed"
fi
# Generate pre-auth key and join tailnet
echo ""
echo " Generating pre-auth key..."
PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create --user server --reusable=false --expiration 1h 2>&1 | tail -1)
echo " Pre-auth key: $PREAUTH_KEY"
echo ""
echo " Joining tailnet..."
tailscale up \
--login-server=https://vpn.jeffemmett.com \
--authkey="$PREAUTH_KEY" \
--hostname=netcup-rs8000 \
--accept-dns=false
# Get the Tailscale IP
TAILSCALE_IP=$(tailscale ip -4)
echo " ✓ Joined tailnet with IP: $TAILSCALE_IP"
# --- Phase 3: Deploy CoreDNS ---
echo ""
echo "[Phase 3] Deploying CoreDNS for *.jefflix.lol..."
mkdir -p /opt/apps/jefflix-dns
# Write Corefile with the actual Tailscale IP
cat > /opt/apps/jefflix-dns/Corefile <<COREFILE_EOF
jefflix.lol {
template IN A {
answer "{{ .Name }} 60 IN A ${TAILSCALE_IP}"
}
log
}
COREFILE_EOF
cat > /opt/apps/jefflix-dns/docker-compose.yml <<COMPOSE_EOF
services:
coredns:
image: coredns/coredns:latest
container_name: jefflix-dns
restart: unless-stopped
command: -conf /etc/coredns/Corefile
volumes:
- ./Corefile:/etc/coredns/Corefile:ro
ports:
- "${TAILSCALE_IP}:53:53/udp"
- "${TAILSCALE_IP}:53:53/tcp"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
COMPOSE_EOF
cd /opt/apps/jefflix-dns && docker compose up -d
echo " ✓ CoreDNS deployed"
# Test DNS
echo ""
echo " Testing DNS resolution..."
sleep 2
DIG_RESULT=$(dig +short @${TAILSCALE_IP} movies.jefflix.lol 2>/dev/null || echo "FAILED")
if [ "$DIG_RESULT" = "$TAILSCALE_IP" ]; then
echo " ✓ DNS test passed: movies.jefflix.lol -> $TAILSCALE_IP"
else
echo " ⚠ DNS test returned: $DIG_RESULT (expected $TAILSCALE_IP)"
echo " Check CoreDNS logs: docker logs jefflix-dns"
fi
# --- Phase 4: Configure Headscale split DNS ---
echo ""
echo "[Phase 4] Configuring Headscale split DNS..."
HEADSCALE_CONFIG="/opt/apps/headscale-deploy/config/config.yaml"
# Check if split DNS is already configured
if grep -q "jefflix.lol" "$HEADSCALE_CONFIG"; then
echo " Split DNS for jefflix.lol already in config, skipping"
else
# Add split DNS section to the nameservers block
# This uses sed to add the split DNS config after the global nameservers
sed -i '/nameservers:/,/^[^ ]/ {
/global:/,/^ [^ ]/ {
/- 1.0.0.1/a\ split:\n jefflix.lol:\n - '"${TAILSCALE_IP}"'
}
}' "$HEADSCALE_CONFIG"
echo " ✓ Split DNS added to Headscale config"
fi
# Restart Headscale
cd /opt/apps/headscale-deploy && docker compose restart headscale
echo " ✓ Headscale restarted"
sleep 3
# Verify Headscale is healthy
docker exec headscale headscale nodes list >/dev/null 2>&1 && echo " ✓ Headscale healthy" || echo " ⚠ Headscale may need attention"
echo ""
echo "========================================"
echo " Setup Complete (Phases 1-4)"
echo "========================================"
echo ""
echo "Server Tailscale IP: $TAILSCALE_IP"
echo "CoreDNS: running on $TAILSCALE_IP:53"
echo "Split DNS: jefflix.lol -> $TAILSCALE_IP"
echo ""
echo "NEXT STEPS:"
echo " 1. Connect YOUR device to the tailnet and test:"
echo " tailscale up --login-server=https://vpn.jeffemmett.com"
echo " dig movies.jefflix.lol (should return $TAILSCALE_IP)"
echo " curl http://movies.jefflix.lol (should return Jellyfin)"
echo ""
echo " 2. Once confirmed working, run cutover.sh to remove public access"
echo " 3. Then onboard users with pre-auth keys"
echo ""