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:
parent
b43a72a080
commit
2c8614e01e
29
README.md
29
README.md
|
|
@ -1,30 +1 @@
|
||||||
# Jefflix website
|
# Jefflix website
|
||||||
|
|
||||||
*Automatically synced with your [v0.app](https://v0.app) deployments*
|
|
||||||
|
|
||||||
[](https://vercel.com/jeff-emmetts-projects/v0-jefflix-website)
|
|
||||||
[](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
|
|
||||||
79
app/page.tsx
79
app/page.tsx
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
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"
|
import { JefflixLogo } from "@/components/jefflix-logo"
|
||||||
|
|
||||||
export default function JefflixPage() {
|
export default function JefflixPage() {
|
||||||
|
|
@ -48,54 +48,36 @@ export default function JefflixPage() {
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
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"
|
variant="default"
|
||||||
>
|
>
|
||||||
<a href="https://movies.jefflix.lol">
|
<a href="https://requests.jefflix.lol">
|
||||||
<Film className="mr-2 h-5 w-5" />
|
<ListPlus className="mr-2 h-5 w-5" />
|
||||||
Movies
|
Request a Show or Movie
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
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"
|
variant="default"
|
||||||
>
|
>
|
||||||
<a href="https://movies.jefflix.lol">
|
<a href="https://movies.jefflix.lol">
|
||||||
<Tv className="mr-2 h-5 w-5" />
|
<Play className="mr-2 h-5 w-5" />
|
||||||
Shows
|
Watch a Show or Movie
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-lg px-8 py-6 font-bold bg-green-600 hover:bg-green-700 text-white"
|
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">
|
<a href="https://upload.jefflix.lol">
|
||||||
<Music className="mr-2 h-5 w-5" />
|
<Upload className="mr-2 h-5 w-5" />
|
||||||
Music
|
Upload Shows or Movies
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -222,20 +204,20 @@ export default function JefflixPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-6">
|
<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">
|
<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://movies.jefflix.lol">
|
<a href="https://requests.jefflix.lol">
|
||||||
<Film className="mr-2 h-5 w-5" />
|
<ListPlus className="mr-2 h-5 w-5" />
|
||||||
Movies
|
Request a Show or Movie
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
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">
|
<a href="https://movies.jefflix.lol">
|
||||||
<Tv className="mr-2 h-5 w-5" />
|
<Play className="mr-2 h-5 w-5" />
|
||||||
Shows
|
Watch a Show or Movie
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -243,28 +225,11 @@ export default function JefflixPage() {
|
||||||
size="lg"
|
size="lg"
|
||||||
className="text-lg px-8 py-6 font-bold bg-green-600 hover:bg-green-700 text-white"
|
className="text-lg px-8 py-6 font-bold bg-green-600 hover:bg-green-700 text-white"
|
||||||
>
|
>
|
||||||
<a href="https://music.jefflix.lol">
|
<a href="https://upload.jefflix.lol">
|
||||||
<Music className="mr-2 h-5 w-5" />
|
<Upload className="mr-2 h-5 w-5" />
|
||||||
Music
|
Upload Shows or Movies
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground pt-4">
|
<p className="text-sm text-muted-foreground pt-4">
|
||||||
Or learn how to set up your own Jellyfin server and join the movement
|
Or learn how to set up your own Jellyfin server and join the movement
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ export default function RequestAccessPage() {
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
This usually happens within 24-48 hours.
|
This usually happens within 24-48 hours.
|
||||||
</p>
|
</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="/">
|
<Link href="/">
|
||||||
<Button variant="outline" className="mt-4">
|
<Button variant="outline" className="mt-4">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
|
@ -159,14 +167,17 @@ export default function RequestAccessPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</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>
|
<h3 className="font-bold mb-2">What happens next?</h3>
|
||||||
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
|
<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>Your request is sent to the admin for review</li>
|
||||||
<li>Once approved, you'll get an email with your login details</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>Request movies & shows at <a href="https://requests.jefflix.lol" className="text-blue-600 hover:underline font-medium">requests.jefflix.lol</a></li>
|
||||||
<li>Live Sports requires an active Sportsnet subscription</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,4 @@ auto_commit: false
|
||||||
bypass_git_hooks: false
|
bypass_git_hooks: false
|
||||||
check_active_branches: true
|
check_active_branches: true
|
||||||
active_branch_days: 30
|
active_branch_days: 30
|
||||||
|
task_prefix: "task"
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
"@radix-ui/react-toggle": "1.1.1",
|
"@radix-ui/react-toggle": "1.1.1",
|
||||||
"@radix-ui/react-toggle-group": "1.1.1",
|
"@radix-ui/react-toggle-group": "1.1.1",
|
||||||
"@radix-ui/react-tooltip": "1.1.6",
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
"@vercel/analytics": "latest",
|
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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 "============================================================"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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 ""
|
||||||
Loading…
Reference in New Issue