Sanitized mirror from private repository - 2026-04-20 01:10:52 UTC
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m2s
Documentation / Deploy to GitHub Pages (push) Has been skipped

This commit is contained in:
Gitea Mirror Bot
2026-04-20 01:10:52 +00:00
commit df23958959
1441 changed files with 363887 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
version: '3'
services:
openhands-app:
image: docker.openhands.dev/openhands/openhands:0.62
container_name: openhands-app
environment:
- OPENHANDS_LLM_PROVIDER=openai
- OPENHANDS_LLM_MODEL=mistralai/devstral-small-2507
- OPENHANDS_LLM_API_BASE=http://192.168.0.253:1234/v1
- OPENHANDS_LLM_API_KEY=dummy
- SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-nikolaik
- LOG_ALL_EVENTS=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/openhands_clean:/.openhands
ports:
- 3000:3000
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,488 @@
# 🎮 NVIDIA Shield TV Pro 4K - Travel Device Configuration
**🟢 Beginner to Intermediate Guide**
The NVIDIA Shield TV Pro serves as a portable homelab access point, providing secure connectivity to your infrastructure while traveling. This guide covers setup, configuration, and usage scenarios.
## 📱 Device Overview
### **Hardware Specifications**
- **Model**: NVIDIA Shield TV Pro (2019)
- **CPU**: NVIDIA Tegra X1+ (8-core, 64-bit ARM)
- **GPU**: 256-core NVIDIA GPU
- **RAM**: 3GB LPDDR4
- **Storage**: 16GB eMMC + microSD expansion
- **Network**: Gigabit Ethernet + 802.11ac WiFi
- **Ports**: 2x USB 3.0, HDMI 2.0b, microSD slot
- **Power**: 20W external adapter
- **Remote**: Voice remote with backlit buttons
- **AI Upscaling**: NVIDIA AI upscaling to 4K
### **Travel Use Cases**
| Scenario | Primary Function | Homelab Integration |
|----------|------------------|-------------------|
| **Hotel Room** | Media streaming, secure browsing | Plex/Jellyfin via Tailscale |
| **Airbnb/Rental** | Personal entertainment system | Full homelab access |
| **Family Visits** | Share media with family | Stream personal library |
| **Business Travel** | Secure work environment | VPN gateway to homelab |
| **Extended Travel** | Portable home setup | Complete service access |
---
## 🔧 Initial Setup & Configuration
### **Step 1: Basic Android TV Setup**
```bash
# Initial device setup
1. Connect to power and HDMI display
2. Follow Android TV setup wizard
3. Sign in with Google account
4. Connect to WiFi network
5. Complete initial updates
6. Enable Developer Options:
- Settings > Device Preferences > About
- Click "Build" 7 times to enable Developer Options
- Settings > Device Preferences > Developer Options
- Enable "USB Debugging"
```
### **Step 2: Enable Sideloading**
```bash
# Allow installation of non-Play Store apps
1. Settings > Device Preferences > Security & Restrictions
2. Enable "Unknown Sources" for apps you trust
3. Or enable per-app when installing Tailscale
```
### **Step 3: Install Essential Apps**
```bash
# Core applications for homelab integration
1. Tailscale (sideloaded)
2. Plex (Play Store)
3. VLC Media Player (Play Store)
4. Chrome Browser (Play Store)
5. Termux (Play Store) - for SSH access
6. Solid Explorer (Play Store) - file management
```
---
## 🌐 Tailscale Configuration
### **Installation Process**
```bash
# Method 1: Direct APK Installation (Recommended)
1. Download Tailscale APK from official website
2. Transfer to Shield via USB drive or network
3. Install using file manager
4. Grant necessary permissions
# Method 2: ADB Installation (Advanced)
# From computer with ADB installed:
adb connect [shield-ip-address]
adb install tailscale.apk
```
### **Tailscale Setup**
```bash
# Initial configuration
1. Open Tailscale app
2. Sign in with your Tailscale account
3. Authorize the device in Tailscale admin console
4. Verify connection to homelab network
5. Test connectivity to homelab services
# Verify connection
# From Termux or ADB shell:
ping atlantis.vish.local
ping 100.83.230.112 # Atlantis Tailscale IP
```
### **Advanced Tailscale Configuration**
```bash
# Configure as exit node (optional)
# Allows Shield to route all traffic through homelab
1. Tailscale admin console > Machines
2. Find NVIDIA Shield device
3. Enable "Exit Node" capability
4. On Shield: Settings > Use as Exit Node
# Subnet routing (if needed)
# Allow access to local networks at travel location
tailscale up --advertise-routes=192.168.1.0/24
```
---
## 📺 Media Streaming Configuration
### **Plex Client Setup**
```bash
# Optimal Plex configuration for travel
1. Install Plex app from Play Store
2. Sign in with Plex account
3. Server should auto-discover via Tailscale
4. If not found manually add:
- Server IP: atlantis.vish.local
- Port: 32400
- Or Tailscale IP: 100.83.230.112:32400
# Quality settings for travel:
# Settings > Video Quality
# - Home Streaming: Maximum (if good WiFi)
# - Remote Streaming: 4 Mbps 720p (for limited bandwidth)
# - Allow Direct Play: Enabled
# - Allow Direct Stream: Enabled
```
### **Alternative Media Apps**
```bash
# Jellyfin (if preferred over Plex)
1. Install Jellyfin app from Play Store
2. Add server: calypso.vish.local:2283
3. Or Tailscale IP: 100.103.48.78:2283
# VLC for direct file access
1. Network streams via SMB/CIFS
2. Direct file playback from NAS
3. Supports all media formats
```
---
## 🔒 Security & VPN Configuration
### **Secure Browsing Setup**
```bash
# Use Shield as secure gateway
1. Configure Tailscale as exit node
2. All traffic routes through homelab
3. Benefits from Pi-hole ad blocking
4. Secure DNS resolution
# Chrome browser configuration:
# - Set homepage to homelab dashboard
# - Bookmark frequently used services
# - Enable sync for consistent experience
```
### **SSH Access to Homelab**
```bash
# Using Termux for SSH connections
1. Install Termux from Play Store
2. Update packages: pkg update && pkg upgrade
3. Install SSH client: pkg install openssh
4. Generate SSH key: ssh-keygen -t ed25519
5. Copy public key to homelab hosts
# Connect to homelab:
ssh admin@atlantis.vish.local
ssh user@homelab-vm.vish.local
ssh pi@concord-nuc.vish.local
```
---
## 🏨 Travel Scenarios & Setup
### **Hotel Room Setup**
```bash
# Quick deployment in hotel room
1. Connect Shield to hotel TV via HDMI
2. Connect to hotel WiFi
3. Launch Tailscale (auto-connects)
4. Access homelab services immediately
5. Stream personal media library
# Hotel WiFi considerations:
# - May need to accept terms via browser
# - Some hotels block VPN traffic
# - Use mobile hotspot as backup
```
### **Airbnb/Rental Property**
```bash
# Extended stay configuration
1. Connect to property WiFi
2. Set up Shield as primary entertainment
3. Configure TV settings for optimal experience
4. Share access with travel companions
5. Use as work environment via homelab
# Family sharing:
# - Create guest Plex accounts
# - Share specific libraries
# - Monitor usage via Tautulli
```
### **Mobile Hotspot Integration**
```bash
# Using phone as internet source
1. Enable mobile hotspot on phone
2. Connect Shield to hotspot WiFi
3. Monitor data usage carefully
4. Adjust streaming quality accordingly
# Data-conscious settings:
# - Plex: 2 Mbps 480p for mobile data
# - Disable automatic updates
# - Use offline content when possible
```
---
## 🎮 Gaming & Entertainment Features
### **GeForce Now Integration**
```bash
# Cloud gaming via NVIDIA's service
1. Install GeForce Now app
2. Sign in with NVIDIA account
3. Access Steam/Epic games library
4. Stream games at 4K 60fps (with good connection)
# Optimal settings:
# - Streaming Quality: Custom
# - Bitrate: Adjust based on connection
# - Frame Rate: 60fps preferred
```
### **Local Game Streaming**
```bash
# Stream games from homelab PCs
1. Install Steam Link app
2. Discover gaming PCs on network
3. Pair with gaming systems
4. Stream games over Tailscale
# Requirements:
# - Gaming PC with Steam installed
# - Good network connection (5+ Mbps)
# - Low latency connection
```
### **Emulation & Retro Gaming**
```bash
# RetroArch for classic games
1. Install RetroArch from Play Store
2. Download cores for desired systems
3. Load ROMs from homelab NAS
4. Configure controllers
# ROM access via SMB:
# - Connect to atlantis.vish.local/roms
# - Browse by system/console
# - Load directly from network storage
```
---
## 🔧 Advanced Configuration
### **Custom Launcher (Optional)**
```bash
# Replace default Android TV launcher
1. Install alternative launcher (FLauncher, ATV Launcher)
2. Set as default home app
3. Customize with homelab shortcuts
4. Create quick access to services
# Homelab shortcuts:
# - Grafana dashboard
# - Portainer interface
# - Plex web interface
# - Router admin panel
```
### **Automation Integration**
```bash
# Home Assistant integration
1. Install Home Assistant app
2. Connect to concord-nuc.vish.local:8123
3. Control smart home devices
4. Automate Shield behavior
# Example automations:
# - Turn on Shield when arriving home
# - Adjust volume based on time of day
# - Switch inputs automatically
```
### **File Management**
```bash
# Solid Explorer configuration
1. Add network locations:
- SMB: //atlantis.vish.local/media
- SMB: //calypso.vish.local/documents
- FTP: homelab-vm.vish.local:21
2. Enable cloud storage integration
3. Set up automatic sync folders
# Use cases:
# - Download files to Shield storage
# - Upload photos/videos to homelab
# - Access documents remotely
```
---
## 📊 Monitoring & Management
### **Performance Monitoring**
```bash
# Monitor Shield performance
1. Settings > Device Preferences > About
2. Check storage usage regularly
3. Monitor network performance
4. Clear cache when needed
# Network diagnostics:
# - WiFi Analyzer app for signal strength
# - Speedtest app for bandwidth testing
# - Ping tools for latency checking
```
### **Remote Management**
```bash
# ADB over network (advanced)
1. Enable ADB over network in Developer Options
2. Connect from computer: adb connect [shield-ip]:5555
3. Execute commands remotely
4. Install/manage apps REDACTED_APP_PASSWORD
# Useful ADB commands:
adb shell pm list packages # List installed apps
adb install app.apk # Install APK remotely
adb shell input keyevent 3 # Simulate home button
adb shell screencap /sdcard/screen.png # Screenshot
```
---
## 🚨 Troubleshooting
### **Common Issues & Solutions**
```bash
# Tailscale connection problems:
1. Check internet connectivity
2. Restart Tailscale app
3. Re-authenticate if needed
4. Verify firewall settings
# Plex streaming issues:
1. Check server status in homelab
2. Test direct IP connection
3. Adjust quality settings
4. Clear Plex app cache
# WiFi connectivity problems:
1. Forget and reconnect to network
2. Check for interference
3. Use 5GHz band if available
4. Reset network settings if needed
```
### **Performance Optimization**
```bash
# Improve Shield performance:
1. Clear app caches regularly
2. Uninstall unused applications
3. Restart device weekly
4. Keep storage under 80% full
# Network optimization:
1. Use wired connection when possible
2. Position close to WiFi router
3. Avoid interference sources
4. Update router firmware
```
---
## 📋 Travel Checklist
### **Pre-Travel Setup**
```bash
☐ Update Shield to latest firmware
☐ Update all apps
☐ Verify Tailscale connectivity
☐ Test Plex streaming
☐ Download offline content if needed
☐ Charge remote control
☐ Pack HDMI cable (if needed)
☐ Pack power adapter
☐ Verify homelab services are running
☐ Set up mobile hotspot backup
```
### **At Destination**
```bash
☐ Connect to local WiFi
☐ Test internet speed
☐ Launch Tailscale
☐ Verify homelab connectivity
☐ Test media streaming
☐ Configure TV settings
☐ Set up any shared access
☐ Monitor data usage (if on mobile)
```
### **Departure Cleanup**
```bash
☐ Sign out of local accounts
☐ Clear browser data
☐ Remove WiFi networks
☐ Reset any personalized settings
☐ Verify no personal data left on device
☐ Pack all accessories
```
---
## 🔗 Integration with Homelab Services
### **Service Access URLs**
```bash
# Via Tailscale (always accessible):
Plex: http://100.83.230.112:32400
Jellyfin: http://100.103.48.78:2283
Grafana: http://100.83.230.112:7099
Home Assistant: http://100.67.40.126:8123
Portainer: http://100.83.230.112:9000
Router Admin: http://192.168.1.1
# Via local DNS (when on home network):
Plex: http://atlantis.vish.local:32400
Jellyfin: http://calypso.vish.local:2283
Grafana: http://atlantis.vish.local:7099
```
### **Backup & Sync**
```bash
# Automatic backup of Shield data
1. Configure Syncthing on Shield (if available)
2. Sync important folders to homelab
3. Backup app configurations
4. Store in homelab for easy restore
# Manual backup process:
1. Use ADB to pull important data
2. Store configurations in homelab Git repo
3. Document custom settings
4. Create restore procedures
```
---
## 📚 Related Documentation
- [Tailscale Setup Guide](../docs/infrastructure/tailscale-setup-guide.md)
- [Travel Networking Guide](../docs/infrastructure/comprehensive-travel-setup.md)
- [Plex Configuration](../docs/services/individual/plex.md)
- [Home Assistant Integration](../docs/services/individual/home-assistant.md)
---
**💡 Pro Tip**: The NVIDIA Shield TV Pro is an incredibly versatile travel companion. With proper setup, it provides seamless access to your entire homelab infrastructure from anywhere in the world, making travel feel like home.
**🔄 Maintenance**: Update this configuration monthly and test all functionality before important trips.

View File

@@ -0,0 +1,5 @@
vish@pi-5-kevin:~/paper $ cat start.sh
#!/bin/bash
java -Xms2G -Xmx4G -jar paper-1.21.7-26.jar nogui
#Run this in a screen

View File

@@ -0,0 +1,67 @@
minecraft server.properties
# Minecraft server properties - Optimized for Raspberry Pi 5 (8GB RAM) Creative Server
# --- Gameplay Settings ---
gamemode=creative
difficulty=peaceful
pvp=false
spawn-protection=0
allow-flight=true
generate-structures=true
level-name=world
level-seed=
level-type=minecraft\:flat
# --- Server Limits & Performance ---
max-players=10
view-distance=6
simulation-distance=4
max-tick-time=100000
sync-chunk-writes=false
entity-broadcast-range-percentage=75
max-world-size=29999984
# --- Networking ---
server-ip=
server-port=25565
rate-limit=10
network-compression-threshold=512
use-native-transport=true
# --- Online Access ---
online-mode=true
enforce-secure-profile=false
prevent-proxy-connections=false
white-list=true
enforce-whitelist=true
# --- RCON/Query (disabled for now) ---
enable-rcon=false
rcon.port=25575
rcon.password=
"REDACTED_PASSWORD"
query.port=25565
# --- Other Options ---
motd=Welcome to Kevin's world
op-permission-level=4
function-permission-level=2
player-idle-timeout=0
text-filtering-config=
text-filtering-version=0
resource-pack=
resource-pack-sha1=
resource-pack-id=
require-resource-pack=false
resource-pack-prompt=
initial-enabled-packs=vanilla
initial-disabled-packs=
bug-report-link=
broadcast-console-to-ops=true
broadcast-rcon-to-ops=true
debug=false
enable-command-block=false
enable-jmx-monitoring=false
pause-when-empty-seconds=-1
accepts-transfers=false

View File

@@ -0,0 +1,28 @@
# Diun — Docker Image Update Notifier
#
# Watches all running containers on this host and sends ntfy
# notifications when upstream images update their digest.
# Schedule: Mondays 09:00 (weekly cadence).
#
# ntfy topic: https://ntfy.vish.gg/diun
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
LOG_LEVEL: info
DIUN_WATCH_WORKERS: "20"
DIUN_WATCH_SCHEDULE: "0 9 * * 1"
DIUN_WATCH_JITTER: 30s
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_NTFY_ENDPOINT: "https://ntfy.vish.gg"
DIUN_NOTIF_NTFY_TOPIC: "diun"
restart: unless-stopped
volumes:
diun-data:

View File

@@ -0,0 +1,15 @@
services:
dozzle-agent:
image: amir20/dozzle:latest
container_name: dozzle-agent
command: agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "7007:7007"
restart: unless-stopped
healthcheck:
test: ["CMD", "/dozzle", "healthcheck"]
interval: 30s
timeout: 5s
retries: 3

View File

@@ -0,0 +1,15 @@
# Glances - Real-time system monitoring
# Web UI: http://<host-ip>:61208
# Provides: CPU, memory, disk, network, Docker container stats
services:
glances:
image: nicolargo/glances:latest
container_name: glances
pid: host
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GLANCES_OPT=--webserver
restart: unless-stopped

View File

@@ -0,0 +1,67 @@
# Immich - Photo/video backup solution
# URL: https://photos.vishconcord.synology.me
# Port: 2283
# Google Photos alternative with ML-powered features
version: "3.8"
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- "2283:2283"
depends_on:
- redis
- database
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:2283/api/server-info"]
interval: 30s
timeout: 5s
retries: 5
# You can enable this later if you really want object detection or face recognition.
# Itll work on the Pi 5, but very, very slowly.
# immich-machine-learning:
# container_name: immich_machine_learning
# image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
# volumes:
# - model-cache:/cache
# env_file:
# - .env
# restart: unless-stopped
# healthcheck:
# disable: false
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 5
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: "REDACTED_PASSWORD"
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: "--data-checksums"
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
restart: unless-stopped
volumes:
model-cache:

View File

@@ -0,0 +1,22 @@
# Samba share on rpi5-vish (192.168.0.66)
# Shares the NVMe storagepool for access by other hosts on the LAN
#
# Mounted by:
# - homelab-vm: /mnt/pi5_storagepool (creds: /etc/samba/.pi5_credentials)
# - Atlantis: /volume1/pi5_storagepool (creds: /root/.pi5_smb_creds)
#
# To apply: copy relevant [storagepool] block into /etc/samba/smb.conf on pi-5
# Set SMB password: "REDACTED_PASSWORD" -e 'PASSWORD\nPASSWORD' | sudo smbpasswd -a vish -s
#
# pi-5 also mounts from Atlantis via NFS:
# /mnt/atlantis_data → 192.168.0.200:/volume1/data (media/torrents/usenet)
[storagepool]
path = /mnt/storagepool
browseable = yes
read only = no
guest ok = no
valid users = vish
force user = vish
create mask = 0664
directory mask = 0775

View File

@@ -0,0 +1,27 @@
# Scrutiny Collector — pi-5 (Raspberry Pi 5)
#
# Ships SMART data to the hub on homelab-vm.
# pi-5 has 2 NVMe drives (M.2 HAT):
# - nvme0n1: Micron 7450 480GB
# - nvme1n1: Samsung 970 EVO Plus 500GB
# NVMe not auto-discovered by smartctl --scan; uses explicit config.
# collector.yaml lives at: /home/vish/scrutiny/collector.yaml
#
# Hub: http://100.67.40.126:8090
services:
scrutiny-collector:
image: ghcr.io/analogj/scrutiny:master-collector
container_name: scrutiny-collector
cap_add:
- SYS_RAWIO
- SYS_ADMIN
volumes:
- /run/udev:/run/udev:ro
- /home/vish/scrutiny/collector.yaml:/opt/scrutiny/config/collector.yaml:ro
devices:
- /dev/nvme0n1
- /dev/nvme1n1
environment:
COLLECTOR_API_ENDPOINT: "http://100.67.40.126:8090"
restart: unless-stopped

View File

@@ -0,0 +1,13 @@
# Uptime Kuma - Self-hosted monitoring tool
# Web UI: http://<host-ip>:3001
# Features: HTTP(s), TCP, Ping, DNS monitoring with notifications
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
network_mode: host
volumes:
- /home/vish/docker/kuma/data:/app/data
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped

View File

View File

@@ -0,0 +1,22 @@
# docker-compose run archivebox init --setup
# docker-compose up
# echo "https://example.com" | docker-compose run archivebox archivebox add
# docker-compose run archivebox add --depth=1 https://example.com/some/feed.rss
# docker-compose run archivebox config --set PUBLIC_INDEX=True
# docker-compose run archivebox help
# Documentation:
# https://github.com/ArchiveBox/ArchiveBox/wiki/Docker#docker-compose
version: '2.4'
services:
archivebox:
image: archivebox/archivebox:master
command: server --quick-init 0.0.0.0:8000
ports:
- 8000:8000
environment:
- ALLOWED_HOSTS=*
- MEDIA_MAX_SIZE=750m
volumes:
- ./data:/data

View File

@@ -0,0 +1,17 @@
# ChatGPT Web - AI chat
# Port: 3000
# ChatGPT web interface
version: '3.9'
services:
deiucanta:
image: 'ghcr.io/deiucanta/chatpad:latest'
restart: unless-stopped
ports:
- '5690:80'
container_name: Chatpad-AI
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,30 @@
# Conduit - Matrix server
# Port: 6167
# Lightweight Matrix homeserver
version: "3.9"
services:
matrix-conduit:
image: matrixconduit/matrix-conduit:latest
container_name: Matrix-Conduit
hostname: matrix-conduit
security_opt:
- no-new-privileges:true
user: 1000:1000
ports:
- "8455:6167"
volumes:
- "/volume1/docker/matrix-conduit:/var/lib/matrix-conduit/"
environment:
- CONDUIT_SERVER_NAME=vishtestingserver
- CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit/
- CONDUIT_DATABASE_BACKEND=rocksdb
- CONDUIT_PORT=6167
- CONDUIT_MAX_REQUEST_SIZE=20000000
- CONDUIT_ALLOW_REGISTRATION=true
- CONDUIT_ALLOW_FEDERATION=true
- CONDUIT_TRUSTED_SERVERS=["matrix.org"]
- CONDUIT_MAX_CONCURRENT_REQUESTS=250
- CONDUIT_ADDRESS=0.0.0.0
- CONDUIT_CONFIG=''
restart: unless-stopped

View File

@@ -0,0 +1,9 @@
version: '3.9'
services:
drawio:
image: jgraph/drawio
restart: unless-stopped
ports:
- '8443:8443'
- '5022:8080'
container_name: drawio

View File

@@ -0,0 +1,15 @@
# Element Web - Matrix client
# Port: 80
# Matrix chat web client
version: '3'
services:
element-web:
image: vectorim/element-web:latest
container_name: element-web
restart: unless-stopped
volumes:
- /home/vish/docker/elementweb/config.json:/app/config.json
ports:
- 9000:80

View File

@@ -0,0 +1,88 @@
# PhotoPrism - Photo management
# Port: 2342
# AI-powered photo management
version: "3.9"
services:
db:
image: mariadb:jammy
container_name: PhotoPrism-DB
hostname: photoprism-db
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
- seccomp:unconfined
- apparmor:unconfined
user: 1000:1000
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -u root -p$$MYSQL_ROOT_PASSWORD | grep 'mysqld is alive' || exit 1"]
volumes:
- /home/vish/docker/photoprism/db:/var/lib/mysql:rw
environment:
TZ: America/Los_Angeles
MYSQL_ROOT_PASSWORD: "REDACTED_PASSWORD"
MYSQL_DATABASE: photoprism
MYSQL_USER: photoprism-user
MYSQL_PASSWORD: "REDACTED_PASSWORD"
restart: on-failure:5
photoprism:
image: photoprism/photoprism:latest
container_name: PhotoPrism
hostname: photoprism
mem_limit: 6g
cpu_shares: 1024
security_opt:
- no-new-privileges:true
- seccomp:unconfined
- apparmor:unconfined
user: 1000:1009
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:2342
ports:
- 2342:2342
volumes:
- /home/vish/docker/photoprism/import:/photoprism/import:rw # *Optional* base folder from which files can be imported to originals
- /home/vish/docker/photoprism/storage:/photoprism/storage:rw
- /home/vish/docker/photoprism/originals:/photoprism/originals:rw
# - /volume1/docker/photoprism/family:/photoprism/originals/family:rw # *Additional* media folders can be mounted like this
environment:
PHOTOPRISM_ADMIN_USER: vish
PHOTOPRISM_ADMIN_PASSWORD: "REDACTED_PASSWORD"
PHOTOPRISM_UID: 1000
PHOTOPRISM_GID: 1000
PHOTOPRISM_AUTH_MODE: password
PHOTOPRISM_SITE_URL: http://localhost:2342/
PHOTOPRISM_ORIGINALS_LIMIT: 5120
PHOTOPRISM_HTTP_COMPRESSION: gzip
PHOTOPRISM_READONLY: false
PHOTOPRISM_EXPERIMENTAL: false
PHOTOPRISM_DISABLE_CHOWN: false
PHOTOPRISM_DISABLE_WEBDAV: false
PHOTOPRISM_DISABLE_SETTINGS: false
PHOTOPRISM_DISABLE_TENSORFLOW: false
PHOTOPRISM_DISABLE_FACES: false
PHOTOPRISM_DISABLE_CLASSIFICATION: false
PHOTOPRISM_DISABLE_RAW: false
PHOTOPRISM_RAW_PRESETS: false
PHOTOPRISM_JPEG_QUALITY: 100
PHOTOPRISM_DETECT_NSFW: false
PHOTOPRISM_UPLOAD_NSFW: true
PHOTOPRISM_SPONSOR: true
PHOTOPRISM_DATABASE_DRIVER: mysql
PHOTOPRISM_DATABASE_SERVER: photoprism-db:3306
PHOTOPRISM_DATABASE_NAME: photoprism
PHOTOPRISM_DATABASE_USER: photoprism-user
PHOTOPRISM_DATABASE_PASSWORD: "REDACTED_PASSWORD"
PHOTOPRISM_WORKERS: 2
PHOTOPRISM_THUMB_FILTER: blackman # best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_APP_MODE: standalone # progressive web app MODE - fullscreen, standalone, minimal-ui, browser
# PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
# PHOTOPRISM_SITE_DESCRIPTION: ""
# PHOTOPRISM_SITE_AUTHOR: ""
working_dir: "/photoprism"
restart: on-failure:5
depends_on:
db:
condition: service_started

View File

@@ -0,0 +1,24 @@
# Pi.Alert - Network scanner
# Port: 20211
# Network device monitoring
version: "3.9"
services:
pi.alert:
container_name: Pi.Alert
healthcheck:
test: curl -f http://localhost:17811/ || exit 1
mem_limit: 2g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /home/vish/docker/pialert/config:/home/pi/pialert/config:rw
- /home/vish/docker/pialert/db:/home/pi/pialert/db:rw
- /home/vish/docker/pialert/logs:/home/pi/pialert/front/log:rw
environment:
TZ: America/Los_Angeles
PORT: 17811
network_mode: host
restart: on-failure:5
image: jokobsk/pi.alert:latest

View File

@@ -0,0 +1,65 @@
# ProxiTok - TikTok frontend
# Port: 8080
# Privacy-respecting TikTok viewer
version: "3.9"
services:
redis:
image: redis
command: redis-server --save 60 1 --loglevel warning
container_name: ProxiTok-REDIS
hostname: proxitok-redis
mem_limit: 256m
cpu_shares: 768
security_opt:
- no-new-privileges:true
read_only: true
user: 1000:1000
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
restart: on-failure:5
signer:
image: ghcr.io/pablouser1/signtok:master
container_name: ProxiTok-SIGNER
hostname: proxitok-signer
mem_limit: 512m
cpu_shares: 768
security_opt:
- no-new-privileges:true
read_only: true
user: 1000:1000
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
restart: on-failure:5
proxitok:
image: ghcr.io/pablouser1/proxitok:master
container_name: ProxiTok
hostname: proxitok
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
healthcheck:
test: stat /etc/passwd || exit 1
ports:
- 9770:80
volumes:
- proxitok-cache:/cache
environment:
LATTE_CACHE: /cache
API_CACHE: redis
REDIS_HOST: proxitok-redis
REDIS_PORT: 6379
API_SIGNER: remote
API_SIGNER_URL: http://proxitok-signer:8080/signature
restart: on-failure:5
depends_on:
redis:
condition: service_healthy
signer:
condition: service_healthy
volumes:
proxitok-cache:

View File

@@ -0,0 +1,145 @@
# Concord NUC
**Hostname**: concord-nuc / vish-concord-nuc
**IP Address**: 192.168.68.100 (static, eno1)
**Tailscale IP**: 100.72.55.21
**OS**: Ubuntu (cloud-init based)
**SSH**: `ssh vish-concord-nuc` (via Tailscale — see `~/.ssh/config`)
---
## Network Configuration
### Static IP Setup
`eno1` is configured with a **static IP** (`192.168.68.100/22`) via netplan. This is required because AdGuard Home binds its DNS listener to a specific IP, and DHCP lease changes would cause it to crash.
**Netplan config**: `/etc/netplan/50-cloud-init.yaml`
```yaml
network:
ethernets:
eno1:
dhcp4: false
addresses:
- 192.168.68.100/22
routes:
- to: default
via: 192.168.68.1
nameservers:
addresses:
- 9.9.9.9
- 1.1.1.1
version: 2
wifis:
wlp1s0:
access-points:
This_Wifi_Sucks:
password: "REDACTED_PASSWORD"
dhcp4: true
```
**Cloud-init is disabled** from managing network config:
`/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg` — prevents reboots from reverting to DHCP.
> **Warning**: If you ever re-enable cloud-init networking or wipe this file, eno1 will revert to DHCP and AdGuard will start crash-looping on the next restart. See the Troubleshooting section below.
---
## Services
| Service | Port | URL |
|---------|------|-----|
| AdGuard Home (Web UI) | 9080 | http://192.168.68.100:9080 |
| AdGuard Home (DNS) | 53 | 192.168.68.100:53, 100.72.55.21:53 |
| Home Assistant | - | see homeassistant.yaml |
| Plex | - | see plex.yaml |
| Syncthing | - | see syncthing.yaml |
| Invidious | 3000 | https://in.vish.gg (public), http://192.168.68.100:3000 |
| Materialious | 3001 | http://192.168.68.100:3001 |
| YourSpotify | 4000, 15000 | see yourspotify.yaml |
---
## Deployed Stacks
| Compose File | Service | Notes |
|-------------|---------|-------|
| `adguard.yaml` | AdGuard Home | DNS ad blocker, binds to 192.168.68.100 |
| `homeassistant.yaml` | Home Assistant | Home automation |
| `plex.yaml` | Plex | Media server |
| `syncthing.yaml` | Syncthing | File sync |
| `wireguard.yaml` | WireGuard / wg-easy | VPN |
| `dyndns_updater.yaml` | DynDNS | Dynamic DNS |
| `node-exporter.yaml` | Node Exporter | Prometheus metrics |
| `piped.yaml` | Piped | YouTube alternative frontend |
| `yourspotify.yaml` | YourSpotify | Spotify stats |
| `invidious/invidious.yaml` | Invidious + Companion + DB + Materialious | YouTube frontend — https://in.vish.gg |
---
## Troubleshooting
### AdGuard crash-loops on startup
**Symptom**: `docker ps` shows AdGuard as "Restarting" or "Up Less than a second"
**Cause**: AdGuard binds DNS to a specific IP (`192.168.68.100`). If the host's IP changes (DHCP), or if AdGuard rewrites its config to the current DHCP address, it will fail to bind on next start.
**Diagnose**:
```bash
docker logs AdGuard --tail 20
# Look for: "bind: cannot assign requested address"
# The log will show which IP it tried to use
```
**Fix**:
```bash
# 1. Check what IP AdGuard thinks it should use
sudo grep -A3 'bind_hosts' /home/vish/docker/adguard/config/AdGuardHome.yaml
# 2. Check what IP eno1 actually has
ip addr show eno1 | grep 'inet '
# 3. If they don't match, update the config
sudo sed -i 's/- 192.168.68.XXX/- 192.168.68.100/' /home/vish/docker/adguard/config/AdGuardHome.yaml
# 4. Restart AdGuard
docker restart AdGuard
```
**If the host IP has reverted to DHCP** (e.g. after a reboot wiped the static config):
```bash
# Re-apply static IP
sudo netplan apply
# Verify
ip addr show eno1 | grep 'inet '
# Should show: inet 192.168.68.100/22
```
---
## Incident History
### 2026-02-22 — AdGuard crash-loop / IP mismatch
- **Root cause**: Host had drifted from `192.168.68.100` to DHCP-assigned `192.168.68.87`. AdGuard briefly started, rewrote its config to `.87`, then the static IP was applied and `.87` was gone — causing a bind failure loop.
- **Resolution**:
1. Disabled cloud-init network management
2. Set `eno1` to static `192.168.68.100/22` via netplan
3. Corrected `AdGuardHome.yaml` `bind_hosts` back to `.100`
4. Restarted AdGuard — stable
---
### 2026-02-27 — Invidious 502 / crash-loop
- **Root cause 1**: PostgreSQL 14 defaults `pg_hba.conf` to `scram-sha-256` for host connections. Invidious's Crystal DB driver does not support scram-sha-256, causing a "password authentication failed" crash loop even with correct credentials.
- **Fix**: Changed last line of `/var/lib/postgresql/data/pg_hba.conf` in the `invidious-db` container from `host all all all scram-sha-256` to `host all all 172.21.0.0/16 trust`, then ran `SELECT pg_reload_conf();`.
- **Root cause 2**: Portainer had saved the literal string `REDACTED_SECRET_KEY` as the `SERVER_SECRET_KEY` env var for the companion container (Portainer's secret-redaction placeholder was baked in as the real value). The latest companion image validates the key strictly (exactly 16 alphanumeric chars), causing it to crash.
- **Fix**: Updated the Portainer stack file via API (`PUT /api/stacks/584`), replacing all `REDACTED_*` placeholders with the real values.
---
*Last updated: 2026-02-27*

View File

@@ -0,0 +1,23 @@
# AdGuard Home - DNS ad blocker
# Web UI: http://192.168.68.100:9080
# DNS: 192.168.68.100:53, 100.72.55.21:53
#
# IMPORTANT: This container binds DNS to 192.168.68.100 (configured in AdGuardHome.yaml).
# The host MUST have a static IP of 192.168.68.100 on eno1, otherwise AdGuard will
# crash-loop with "bind: cannot assign requested address".
# See README.md for static IP setup and troubleshooting.
services:
adguard:
image: adguard/adguardhome
container_name: AdGuard
mem_limit: 2g
cpu_shares: 768
security_opt:
- no-new-privileges:true
restart: unless-stopped
network_mode: host
volumes:
- /home/vish/docker/adguard/config:/opt/adguardhome/conf:rw
- /home/vish/docker/adguard/data:/opt/adguardhome/work:rw
environment:
TZ: America/Los_Angeles

View File

@@ -0,0 +1,28 @@
# Diun — Docker Image Update Notifier
#
# Watches all running containers on this host and sends ntfy
# notifications when upstream images update their digest.
# Schedule: Mondays 09:00 (weekly cadence).
#
# ntfy topic: https://ntfy.vish.gg/diun
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
LOG_LEVEL: info
DIUN_WATCH_WORKERS: "20"
DIUN_WATCH_SCHEDULE: "0 9 * * 1"
DIUN_WATCH_JITTER: 30s
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_NTFY_ENDPOINT: "https://ntfy.vish.gg"
DIUN_NOTIF_NTFY_TOPIC: "diun"
restart: unless-stopped
volumes:
diun-data:

View File

@@ -0,0 +1,28 @@
pds-g^KU_n-Ck6JOm^BQu9pcct0DI/MvsCnViM6kGHGVCigvohyf/HHHfHG8c=
8. Start the Server
Use screen or tmux to keep the server running in the background.
Start Master (Overworld) Server
bash
Copy
Edit
cd ~/dst/bin
screen -S dst-master ./dontstarve_dedicated_server_nullrenderer -cluster MyCluster -shard Master
Start Caves Server
Open a new session:
bash
Copy
Edit
screen -S dst-caves ./dontstarve_dedicated_server_nullrenderer -cluster MyCluster -shard Caves
[Service]
User=dst
ExecStart=/home/dstserver/dst/bin/dontstarve_dedicated_server_nullrenderer -cluster MyCluster -shard Master
Restart=always

View File

@@ -0,0 +1,15 @@
services:
dozzle-agent:
image: amir20/dozzle:latest
container_name: dozzle-agent
command: agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "7007:7007"
restart: unless-stopped
healthcheck:
test: ["CMD", "/dozzle", "healthcheck"]
interval: 30s
timeout: 5s
retries: 3

View File

@@ -0,0 +1,17 @@
# Dynamic DNS Updater
# Updates DNS records when public IP changes
version: '3.8'
services:
ddns-vish-13340:
image: favonia/cloudflare-ddns:latest
network_mode: host
restart: unless-stopped
user: "1000:1000"
read_only: true
cap_drop: [all]
security_opt: [no-new-privileges:true]
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- DOMAINS=api.vish.gg,api.vp.vish.gg,in.vish.gg
- PROXIED=false

View File

@@ -0,0 +1,55 @@
# Home Assistant - Smart home automation
# Port: 8123
# Open source home automation platform
version: '3'
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
network_mode: host
restart: unless-stopped
environment:
- TZ=America/Los_Angeles
volumes:
- /home/vish/docker/homeassistant:/config
- /etc/localtime:/etc/localtime:ro
matter-server:
container_name: matter-server
image: ghcr.io/home-assistant-libs/python-matter-server:stable
network_mode: host
restart: unless-stopped
volumes:
- /home/vish/docker/matter:/data
piper:
container_name: piper
image: rhasspy/wyoming-piper:latest
restart: unless-stopped
ports:
- "10200:10200"
volumes:
- /home/vish/docker/piper:/data
command: --voice en_US-lessac-medium
whisper:
container_name: whisper
image: rhasspy/wyoming-whisper:latest
restart: unless-stopped
ports:
- "10300:10300"
volumes:
- /home/vish/docker/whisper:/data
command: --model tiny-int8 --language en
openwakeword:
container_name: openwakeword
image: rhasspy/wyoming-openwakeword:latest
restart: unless-stopped
ports:
- "10400:10400"
command: --preload-model ok_nabu
networks:
default:
name: homeassistant-stack

View File

@@ -0,0 +1,34 @@
# Concord NUC Home Assistant — Credentials
Private repo — real secrets committed per repo policy.
Public mirror (`homelab-optimized`) will substitute REDACTED_* placeholders.
## Oura Ring (OAuth2)
Application credentials created at [cloud.ouraring.com → Personal → API](https://cloud.ouraring.com/).
Used by the **built-in** Home Assistant `oura` integration (HA 2024.4+, OAuth2 via application credentials).
- **Client ID**: `6dec0bb3-0739-4323-9a04-11ea8d05bdaa` <!-- pragma: allowlist secret -->
- **Client Secret**: `gG___3Eeb4AYfUjpUyyqBqI3CZqFTjWOJ4-osIIqqYs` <!-- pragma: allowlist secret -->
- **Redirect URI configured in Oura**: `https://my.home-assistant.io/redirect/oauth` (default HA flow)
### How to add to Home Assistant
1. Go to `Settings → Devices & Services → Helpers → Application Credentials → Add Credential`
(or the integration setup flow will prompt for these on first run).
2. Select **Oura** as the integration.
3. Paste the Client ID and Client Secret above.
4. Then `Settings → Devices & Services → Add Integration → Oura` → complete OAuth flow.
### Entities exposed (once integrated)
Expected entities created by the built-in integration:
- `sensor.oura_readiness_score`
- `sensor.oura_sleep_score`
- `sensor.oura_activity_score`
- `sensor.oura_resting_heart_rate`
- `sensor.oura_heart_rate_variability`
- `sensor.oura_total_sleep`
- `sensor.oura_time_in_bed`
(Exact entity IDs may differ — verify after first integration run and update `dashboards/bedroom.yaml` accordingly.)

View File

@@ -0,0 +1,115 @@
# Frigate NVR — Deployment Plan
Not yet deployed. The HACS `custom_components/frigate` integration is already on the HA instance, but no Frigate server exists to talk to.
## What Frigate brings
- Object detection (person/car/dog/etc.) on camera streams, with far fewer false-positives than the Tapo built-in motion/person detection.
- Timeline of detected events with snapshot + 10s clip per event.
- RTSP relay + re-stream (can replace go2rtc in HA for this camera).
- HA integration exposes: `binary_sensor.<camera>_person_occupancy`, `sensor.<camera>_person_count`, `image.<camera>_person_snapshot`, `camera.<camera>_person` image for the latest detection, and per-zone variants.
## Stack required on concord-nuc
```yaml
# concord_nuc/frigate.yaml (new compose to add)
services:
frigate-mqtt:
image: eclipse-mosquitto:2
container_name: frigate-mqtt
restart: unless-stopped
ports:
- "1883:1883"
volumes:
- /home/vish/docker/mosquitto/config:/mosquitto/config
- /home/vish/docker/mosquitto/data:/mosquitto/data
frigate:
image: ghcr.io/blakeblackshear/frigate:stable
container_name: frigate
restart: unless-stopped
privileged: true # for /dev/dri passthrough if using Intel QuickSync
shm_size: 512mb
ports:
- "5000:5000" # web UI
- "8554:8554" # RTSP relay
- "8555:8555/tcp"
- "8555:8555/udp" # WebRTC
volumes:
- /etc/localtime:/etc/localtime:ro
- /home/vish/docker/frigate/config:/config
- /home/vish/docker/frigate/media:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
devices:
- /dev/dri/renderD128 # Intel iGPU (NUC6i3SYB has HD 520)
depends_on:
- frigate-mqtt
```
## Frigate config (`/home/vish/docker/frigate/config/config.yml`)
```yaml
mqtt:
host: frigate-mqtt
port: 1883
cameras:
bedroom:
ffmpeg:
inputs:
- path: rtsp://vishinator:<PASSWORD>@192.168.68.67:554/stream1
roles: [detect, record]
detect:
width: 1920
height: 1080
fps: 5
objects:
track: [person, cat, dog]
record:
enabled: true
retain:
days: 7
mode: motion
snapshots:
enabled: true
retain:
default: 30
# NUC6i3 has no NPU/GPU for ML — use CPU detector
detectors:
cpu1:
type: cpu
num_threads: 3
```
## HA integration wiring
The `custom_components/frigate` integration config entry needs:
- `url`: `http://localhost:5000` (Frigate UI on concord-nuc)
- `password`: (if auth enabled)
Add via **Settings → Devices & Services → Add Integration → Frigate**.
## Dashboard changes (after deployment)
Update `dashboards/cameras.yaml` Bedroom section:
- Replace `camera.vish_bedroom_camera_4k_hd_stream` with `camera.bedroom_frigate` (Frigate's re-stream)
- Add `binary_sensor.bedroom_person_occupancy` to picture-glance entities
- Add a "Recent Events" card: `custom:frigate-card` or a logbook filtered to Frigate sensors
## Storage considerations
- 7-day retention with motion-mode recording on one 1080p/5fps stream ≈ 30-60 GB on the NUC's SSD (currently 73% full at 26 GB free).
- Either allocate a larger disk, **change retain days to 2-3**, or mount `/media/frigate` to an NFS share on Atlantis (has ~TB free).
## Why deferred
1. Takes 1-2h to set up cleanly (MQTT configured, RTSP credentials tested, model tuned).
2. NUC6i3 CPU is weak — detection on a single 1080p@5fps stream will probably max out the CPU. An Intel Coral TPU ($75) would fix this.
3. The existing Tapo built-in motion/person detection already surfaces to HA via `tapo_control`; it's noisy but functional.
Pick this back up when:
- You want real person-detection events + timeline
- OR you add more cameras (Tapo's per-camera detection doesn't scale well)
- OR you buy a Coral TPU

View File

@@ -0,0 +1,77 @@
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder + inject Google Fonts
frontend:
themes: !include_dir_merge_named themes
extra_module_url:
- /local/fonts-loader.js
- /local/assist-fix.js
# Legacy includes (kept for back-compat)
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# REST sensors for homelab services (Sonarr, Radarr, Bazarr, SABnzbd, LazyLibrarian, ABS, Plex)
sensor: !include sensors.yaml
# Custom YAML-mode dashboards (per-room + cameras)
# Default "Overview" dashboard stays in storage mode.
lovelace:
mode: storage
dashboards:
home-view:
mode: yaml
title: Home
icon: mdi:home
show_in_sidebar: true
filename: dashboards/home.yaml
living-room:
mode: yaml
title: Living Room
icon: mdi:sofa
show_in_sidebar: true
filename: dashboards/livingroom.yaml
kitchen-view:
mode: yaml
title: Kitchen
icon: mdi:stove
show_in_sidebar: true
filename: dashboards/kitchen.yaml
bathroom-view:
mode: yaml
title: Bathroom
icon: mdi:shower
show_in_sidebar: true
filename: dashboards/bathroom.yaml
bedroom-view:
mode: yaml
title: Bedroom
icon: mdi:bed
show_in_sidebar: true
filename: dashboards/bedroom.yaml
cameras-view:
mode: yaml
title: Cameras
icon: mdi:cctv
show_in_sidebar: true
filename: dashboards/cameras.yaml
homelab-view:
mode: yaml
title: Homelab
icon: mdi:server
show_in_sidebar: true
filename: dashboards/homelab.yaml
homelab-web:
mode: yaml
title: Homelab Web
icon: mdi:home-analytics
show_in_sidebar: true
filename: dashboards/homelab_web.yaml
crista-web:
mode: yaml
title: Crista
icon: mdi:heart
show_in_sidebar: true
filename: dashboards/crista.yaml

View File

@@ -0,0 +1,165 @@
title: Bathroom
views:
- type: sections
title: Bathroom
path: bathroom
icon: mdi:shower
max_columns: 2
sections:
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:shower
heading: Bathroom
heading_style: title
- type: grid
columns: 3
square: false
cards:
- type: tile
entity: light.bathroom_light_1
name: Light 1
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.bathroom_light_2
name: Light 2
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.bathroom_light_3
name: Light 3
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.bathroom_light_4
name: Light 4
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.bathroom_light_5
name: Light 5
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.bathroom_light_6
name: Light 6
icon: mdi:lightbulb
vertical: true
features:
- type: light-brightness
- type: horizontal-stack
cards:
- type: button
name: All On
icon: mdi:lightbulb-group
tap_action:
action: call-service
service: light.turn_on
target:
entity_id:
- light.bathroom_light_1
- light.bathroom_light_2
- light.bathroom_light_3
- light.bathroom_light_4
- light.bathroom_light_5
- light.bathroom_light_6
- type: button
name: All Off
icon: mdi:lightbulb-group-off
tap_action:
action: call-service
service: light.turn_off
target:
entity_id:
- light.bathroom_light_1
- light.bathroom_light_2
- light.bathroom_light_3
- light.bathroom_light_4
- light.bathroom_light_5
- light.bathroom_light_6
- type: button
name: Relax
icon: mdi:bathtub
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 35
color_temp_kelvin: 2400
target:
entity_id:
- light.bathroom_light_1
- light.bathroom_light_2
- light.bathroom_light_3
- light.bathroom_light_4
- light.bathroom_light_5
- light.bathroom_light_6
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:chart-line
heading: Health
heading_style: title
- type: entities
title: Bulb Status
show_header_toggle: false
entities:
- entity: binary_sensor.bathroom_light_1_cloud_connection
name: Light 1 - Cloud
- entity: binary_sensor.bathroom_light_1_overheated
name: Light 1 - Overheat
- entity: binary_sensor.bathroom_light_2_cloud_connection
name: Light 2 - Cloud
- entity: binary_sensor.bathroom_light_2_overheated
name: Light 2 - Overheat
- entity: binary_sensor.bathroom_light_3_cloud_connection
name: Light 3 - Cloud
- entity: binary_sensor.bathroom_light_3_overheated
name: Light 3 - Overheat
- entity: binary_sensor.bathroom_light_4_cloud_connection
name: Light 4 - Cloud
- entity: binary_sensor.bathroom_light_4_overheated
name: Light 4 - Overheat
- entity: binary_sensor.bathroom_light_5_cloud_connection
name: Light 5 - Cloud
- entity: binary_sensor.bathroom_light_5_overheated
name: Light 5 - Overheat
- entity: binary_sensor.bathroom_light_6_cloud_connection
name: Light 6 - Cloud
- entity: binary_sensor.bathroom_light_6_overheated
name: Light 6 - Overheat
- type: entities
title: Signal Strength
show_header_toggle: false
entities:
- entity: sensor.bathroom_light_1_signal_level
- entity: sensor.bathroom_light_2_signal_level
- entity: sensor.bathroom_light_3_signal_level
- entity: sensor.bathroom_light_4_signal_level
- entity: sensor.bathroom_light_5_signal_level
- entity: sensor.bathroom_light_6_signal_level
- type: markdown
content: |
_Room has no dedicated motion/humidity sensors.
Consider adding a [Tapo T110](https://www.tapo.com) motion sensor
or an Aqara Zigbee multi-sensor once the GL-S200 Thread BR arrives._

View File

@@ -0,0 +1,386 @@
title: Bedroom
views:
- type: sections
title: Bedroom
path: bedroom
icon: mdi:bed
max_columns: 3
sections:
# ---- Lights ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:lightbulb-multiple
heading: Lights
heading_style: title
- type: tile
entity: light.vish_bedroom_light_1
name: Light 1
vertical: true
features:
- type: light-brightness
- type: light-color-temp
- type: tile
entity: light.vish_bedroom_light_2
name: Light 2
vertical: true
features:
- type: light-brightness
- type: light-color-temp
- type: tile
entity: light.vish_bedroom_light_3
name: Light 3
vertical: true
features:
- type: light-brightness
- type: light-color-temp
- type: horizontal-stack
cards:
- type: button
name: All On
icon: mdi:lightbulb-group
tap_action:
action: call-service
service: light.turn_on
target:
entity_id:
- light.vish_bedroom_light_1
- light.vish_bedroom_light_2
- light.vish_bedroom_light_3
- type: button
name: All Off
icon: mdi:lightbulb-group-off
tap_action:
action: call-service
service: light.turn_off
target:
entity_id:
- light.vish_bedroom_light_1
- light.vish_bedroom_light_2
- light.vish_bedroom_light_3
- type: horizontal-stack
cards:
- type: button
name: Read
icon: mdi:book-open-page-variant
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 80
color_temp_kelvin: 3500
target:
entity_id:
- light.vish_bedroom_light_1
- light.vish_bedroom_light_2
- light.vish_bedroom_light_3
- type: button
name: Sleep
icon: mdi:weather-night
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 8
color_temp_kelvin: 2200
target:
entity_id:
- light.vish_bedroom_light_1
- light.vish_bedroom_light_2
- light.vish_bedroom_light_3
# ---- Camera ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:cctv
heading: Bedroom Camera
heading_style: title
- type: picture-glance
title: Bedroom 4K
camera_view: live
camera_image: camera.vish_bedroom_camera_4k_hd_stream
entities:
- binary_sensor.vish_bedroom_camera_4k_motion_alarm
- binary_sensor.vish_bedroom_camera_4k_person_detection
- switch.vish_bedroom_camera_4k_privacy
- light.vish_bedroom_camera_4k_floodlight_timed
- siren.vish_bedroom_camera_4k_siren
- type: horizontal-stack
cards:
- type: button
name: Up
icon: mdi:arrow-up-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_tilt_up
- type: button
name: Down
icon: mdi:arrow-down-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_tilt_down
- type: button
name: Left
icon: mdi:arrow-left-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_pan_left
- type: button
name: Right
icon: mdi:arrow-right-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_pan_right
- type: heading
icon: mdi:cctv-off
heading: Detection
heading_style: subtitle
- type: grid
columns: 2
square: false
cards:
- type: tile
entity: switch.vish_bedroom_camera_4k_motion_detection
name: Motion
icon: mdi:motion-sensor
vertical: false
tap_action:
action: toggle
- type: tile
entity: switch.vish_bedroom_camera_4k_person_detection
name: Person
icon: mdi:account-search
vertical: false
tap_action:
action: toggle
- type: tile
entity: switch.vish_bedroom_camera_4k_privacy
name: Privacy
icon: mdi:eye-off
vertical: false
tap_action:
action: toggle
- type: tile
entity: select.vish_bedroom_camera_4k_night_vision
name: Night Vision
icon: mdi:weather-night
vertical: false
tap_action:
action: more-info
# ---- Media ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:cast
heading: Media
heading_style: title
- type: media-control
entity: media_player.tv_bedroom
- type: media-control
entity: media_player.bedroom_display
- type: media-control
entity: media_player.spotify_vish_khemraj
# ---- Sleep / Oura ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:ring
heading: Oura Ring
heading_style: title
# Hero scores
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.oura_ring_sleep_score
name: Sleep
min: 0
max: 100
severity:
green: 85
yellow: 70
red: 0
needle: true
- type: gauge
entity: sensor.oura_ring_readiness_score
name: Readiness
min: 0
max: 100
severity:
green: 85
yellow: 70
red: 0
needle: true
- type: gauge
entity: sensor.oura_ring_activity_score
name: Activity
min: 0
max: 100
severity:
green: 85
yellow: 70
red: 0
needle: true
- type: gauge
entity: sensor.oura_ring_stress_resilience_score
name: Resilience
min: 0
max: 100
severity:
green: 70
yellow: 50
red: 0
needle: true
# Sleep breakdown
- type: entities
title: Last Night
show_header_toggle: false
state_color: true
entities:
- entity: sensor.oura_ring_total_sleep_duration
name: Total Sleep
icon: mdi:bed-clock
- entity: sensor.oura_ring_deep_sleep_duration
name: Deep
icon: mdi:water
- entity: sensor.oura_ring_rem_sleep_duration
name: REM
icon: mdi:eye
- entity: sensor.oura_ring_light_sleep_duration
name: Light
icon: mdi:weather-sunset
- entity: sensor.oura_ring_awake_time
name: Awake
icon: mdi:eye-outline
- entity: sensor.oura_ring_sleep_efficiency
name: Efficiency
icon: mdi:percent
- entity: sensor.oura_ring_bedtime_start
name: Bedtime
icon: mdi:clock-start
- entity: sensor.oura_ring_bedtime_end
name: Wake
icon: mdi:clock-end
# Trends
- type: history-graph
title: Sleep & Readiness (14d)
hours_to_show: 336
entities:
- entity: sensor.oura_ring_sleep_score
- entity: sensor.oura_ring_readiness_score
- type: history-graph
title: HRV & Resting HR (14d)
hours_to_show: 336
entities:
- entity: sensor.oura_ring_average_sleep_hrv
- entity: sensor.oura_ring_lowest_sleep_heart_rate
# Vitals + activity
- type: horizontal-stack
cards:
- type: tile
entity: sensor.oura_ring_average_sleep_hrv
name: Avg HRV
icon: mdi:heart-pulse
- type: tile
entity: sensor.oura_ring_lowest_sleep_heart_rate
name: Low HR
icon: mdi:heart
- type: tile
entity: sensor.oura_ring_temperature_deviation
name: Temp Δ
icon: mdi:thermometer
- type: tile
entity: sensor.oura_ring_vo2_max
name: VO₂ Max
icon: mdi:lungs
- type: horizontal-stack
cards:
- type: tile
entity: sensor.oura_ring_steps
name: Steps
icon: mdi:shoe-print
- type: tile
entity: sensor.oura_ring_active_calories
name: Active Cal
icon: mdi:fire
- type: tile
entity: sensor.oura_ring_cardiovascular_age
name: CV Age
icon: mdi:heart-cog
- type: tile
entity: binary_sensor.oura_ring_rest_mode
name: Rest Mode
icon: mdi:sleep
- type: conditional
conditions:
- condition: state
entity: sensor.oura_ring_low_battery_alert
state: "on"
card:
type: tile
entity: sensor.oura_ring_low_battery_alert
name: Ring Battery Low
icon: mdi:battery-alert
# ---- Power / Environment ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:flash
heading: Guava Power
heading_style: title
- type: tile
entity: switch.guava_energy
name: Guava (TrueNAS) Plug
icon: mdi:server-network
vertical: true
tap_action:
action: toggle
- type: entities
show_header_toggle: false
entities:
- entity: sensor.guava_energy_current_consumption
name: Power Now
- entity: sensor.guava_energy_today_s_consumption
name: Today
- entity: sensor.guava_energy_this_month_s_consumption
name: This Month
- entity: sensor.guava_energy_voltage
name: Voltage
- entity: sensor.guava_energy_current
name: Current

View File

@@ -0,0 +1,165 @@
title: Cameras
views:
- type: sections
title: Live
path: live
icon: mdi:cctv
max_columns: 2
sections:
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:bed
heading: Bedroom 4K
heading_style: title
- type: picture-glance
title: Bedroom - HD
camera_view: live
camera_image: camera.vish_bedroom_camera_4k_hd_stream
entities:
- binary_sensor.vish_bedroom_camera_4k_motion_alarm
- binary_sensor.vish_bedroom_camera_4k_person_detection
- switch.vish_bedroom_camera_4k_privacy
- light.vish_bedroom_camera_4k_floodlight_timed
- type: horizontal-stack
cards:
- type: button
icon: mdi:arrow-up-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_tilt_up
- type: button
icon: mdi:arrow-down-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_tilt_down
- type: button
icon: mdi:arrow-left-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_pan_left
- type: button
icon: mdi:arrow-right-bold
tap_action:
action: call-service
service: button.press
target:
entity_id: button.vish_bedroom_camera_4k_pan_right
- type: heading
icon: mdi:cctv-off
heading: Detection Settings
heading_style: subtitle
- type: grid
columns: 2
square: false
cards:
- type: tile
entity: switch.vish_bedroom_camera_4k_motion_detection
name: Motion
icon: mdi:motion-sensor
tap_action:
action: toggle
- type: tile
entity: switch.vish_bedroom_camera_4k_person_detection
name: Person
icon: mdi:account-search
tap_action:
action: toggle
- type: tile
entity: switch.vish_bedroom_camera_4k_privacy
name: Privacy
icon: mdi:eye-off
tap_action:
action: toggle
- type: tile
entity: select.vish_bedroom_camera_4k_night_vision
name: Night Vision
icon: mdi:weather-night
tap_action:
action: more-info
- type: tile
entity: select.vish_bedroom_camera_4k_patrol_mode
name: Patrol
icon: mdi:shield-search
tap_action:
action: more-info
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:sofa
heading: Living Room
heading_style: title
- type: picture-glance
title: Living Room
camera_view: live
camera_image: camera.192_168_69_116
entities: []
- type: heading
icon: mdi:home-outline
heading: Other Cameras
heading_style: subtitle
- type: picture-glance
title: Camera (192.168.68.67)
camera_view: live
camera_image: camera.192_168_68_67
entities: []
- type: heading
icon: mdi:cctv
heading: Setillo (Estudio)
heading_style: subtitle
- type: picture-glance
title: Estudio (Surveillance Station)
camera_view: live
camera_image: camera.estudio
entities:
- switch.setillo_surveillance_station_home_mode
- type: conditional
conditions:
- condition: state
entity: camera.192_168_12_155
state_not: unavailable
card:
type: picture-glance
title: Hawaii Camera
camera_view: live
camera_image: camera.192_168_12_155
entities: []
- type: sections
title: Events
path: events
icon: mdi:motion-sensor
sections:
- type: grid
cards:
- type: heading
icon: mdi:motion-sensor
heading: Recent Motion
heading_style: title
- type: logbook
hours_to_show: 24
entities:
- binary_sensor.vish_bedroom_camera_4k_motion_alarm
- binary_sensor.vish_bedroom_camera_4k_person_detection
- binary_sensor.vish_bedroom_camera_4k_cell_motion_detection

View File

@@ -0,0 +1,10 @@
title: Crista
views:
- title: crista.love
path: crista
type: panel
icon: mdi:heart
cards:
- type: iframe
url: https://crista.love
aspect_ratio: "100%"

View File

@@ -0,0 +1,361 @@
title: Home
views:
- type: sections
title: Home
path: home
icon: mdi:home
max_columns: 3
sections:
# ---- Greeting + presence ----
- type: grid
column_span: 3
cards:
- type: custom:mushroom-template-card
primary: >-
{% set t = now().hour %}
{% if t < 5 %}Good night, Vish
{% elif t < 12 %}Good morning, Vish
{% elif t < 17 %}Good afternoon, Vish
{% elif t < 21 %}Good evening, Vish
{% else %}Good night, Vish{% endif %}
secondary: >-
{{ as_timestamp(now()) | timestamp_custom('%A, %B %-d • %-I:%M %p') }}
icon: mdi:home-heart
icon_color: >-
{% set t = now().hour %}
{% if t < 6 or t > 20 %}indigo
{% elif t < 10 %}amber
{% elif t < 17 %}blue
{% else %}deep-orange{% endif %}
tap_action:
action: none
- type: custom:mushroom-chips-card
alignment: center
chips:
- type: entity
entity: weather.forecast_home
icon_color: blue
tap_action:
action: more-info
- type: entity
entity: sensor.oura_ring_readiness_score
name: Readiness
icon: mdi:ring
icon_color: green
content_info: state
tap_action:
action: more-info
- type: entity
entity: sensor.oura_ring_sleep_score
name: Sleep
icon: mdi:sleep
icon_color: indigo
content_info: state
- type: entity
entity: sensor.atlantis
name: Plex
icon: mdi:plex
icon_color: orange
content_info: state
- type: entity
entity: sensor.adguard_home_dns_queries_blocked_ratio
name: AdGuard
icon: mdi:shield-check
icon_color: teal
content_info: state
- type: entity
entity: sensor.speedtest_download
icon: mdi:download-network
icon_color: cyan
content_info: state
- type: entity
entity: sensor.atlantis_cpu_utilization_total
name: NAS CPU
icon: mdi:nas
icon_color: blue
content_info: state
- type: entity
entity: sensor.pve_cpu_usage
name: PVE CPU
icon: mdi:server-network
icon_color: green
content_info: state
# ---- Persons ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:account-multiple
heading: People
heading_style: title
- type: custom:mushroom-person-card
entity: person.vish
layout: horizontal
primary_info: name
secondary_info: state
icon_type: entity-picture
- type: custom:mushroom-person-card
entity: person.crista
layout: horizontal
primary_info: name
secondary_info: state
icon_type: entity-picture
# ---- Weather hero ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:weather-partly-cloudy
heading: Weather
heading_style: title
- type: weather-forecast
entity: weather.forecast_home
forecast_type: daily
# ---- Oura hero ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:ring
heading: Oura Today
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.oura_ring_readiness_score
name: Ready
min: 0
max: 100
needle: true
severity:
green: 85
yellow: 70
red: 0
- type: gauge
entity: sensor.oura_ring_sleep_score
name: Sleep
min: 0
max: 100
needle: true
severity:
green: 85
yellow: 70
red: 0
- type: custom:mushroom-template-card
primary: "Slept {{ states('sensor.oura_ring_total_sleep_duration') }} hrs"
secondary: >-
HRV {{ states('sensor.oura_ring_average_sleep_hrv') }} •
HR {{ states('sensor.oura_ring_lowest_sleep_heart_rate') }}
icon: mdi:bed-clock
icon_color: indigo
# ---- Rooms nav ----
- type: grid
column_span: 3
cards:
- type: heading
icon: mdi:floor-plan
heading: Rooms
heading_style: title
- type: grid
columns: 5
square: false
cards:
- type: custom:mushroom-template-card
primary: Living Room
secondary: >-
{% set lights = [] %}
{% if states('media_player.tv_living_room') != 'off' %}TV on{% else %}{{ states('sensor.speedtest_download') }} Mbps ↓{% endif %}
icon: mdi:sofa
icon_color: >-
{% if states('media_player.tv_living_room') not in ['off','standby','unavailable'] %}deep-orange{% else %}grey{% endif %}
tap_action:
action: navigate
navigation_path: /living-room/living-room
- type: custom:mushroom-template-card
primary: Kitchen
secondary: >-
{% set lights = ['light.kitchen_above_sink','light.kitchen_light_1','light.kitchen_light_2','light.kitchen_light_3','light.kitchen_light_4'] %}
{% set on = lights | select('is_state','on') | list | count %}
{{ on }}/{{ lights|count }} lights on
icon: mdi:stove
icon_color: >-
{% set lights = ['light.kitchen_above_sink','light.kitchen_light_1','light.kitchen_light_2','light.kitchen_light_3','light.kitchen_light_4'] %}
{% set on = lights | select('is_state','on') | list | count %}
{% if on > 0 %}amber{% else %}grey{% endif %}
tap_action:
action: navigate
navigation_path: /kitchen-view/kitchen
- type: custom:mushroom-template-card
primary: Bathroom
secondary: >-
{% set lights = ['light.bathroom_light_1','light.bathroom_light_2','light.bathroom_light_3','light.bathroom_light_4','light.bathroom_light_5','light.bathroom_light_6'] %}
{% set on = lights | select('is_state','on') | list | count %}
{{ on }}/{{ lights|count }} lights on
icon: mdi:shower
icon_color: >-
{% set lights = ['light.bathroom_light_1','light.bathroom_light_2','light.bathroom_light_3','light.bathroom_light_4','light.bathroom_light_5','light.bathroom_light_6'] %}
{% set on = lights | select('is_state','on') | list | count %}
{% if on > 0 %}amber{% else %}grey{% endif %}
tap_action:
action: navigate
navigation_path: /bathroom-view/bathroom
- type: custom:mushroom-template-card
primary: Bedroom
secondary: >-
{% set lights = ['light.vish_bedroom_light_1','light.vish_bedroom_light_2','light.vish_bedroom_light_3'] %}
{% set on = lights | select('is_state','on') | list | count %}
{{ on }}/{{ lights|count }} • {{ states('switch.guava_energy') }} plug
icon: mdi:bed
icon_color: >-
{% set lights = ['light.vish_bedroom_light_1','light.vish_bedroom_light_2','light.vish_bedroom_light_3'] %}
{% set on = lights | select('is_state','on') | list | count %}
{% if on > 0 %}amber{% else %}purple{% endif %}
tap_action:
action: navigate
navigation_path: /bedroom-view/bedroom
- type: custom:mushroom-template-card
primary: Cameras
secondary: >-
{% if is_state('binary_sensor.vish_bedroom_camera_4k_motion_alarm','on') %}⚠ Motion
{% elif is_state('switch.vish_bedroom_camera_4k_privacy','on') %}Privacy on
{% else %}Armed{% endif %}
icon: mdi:cctv
icon_color: >-
{% if is_state('binary_sensor.vish_bedroom_camera_4k_motion_alarm','on') %}red
{% elif is_state('switch.vish_bedroom_camera_4k_privacy','on') %}grey
{% else %}teal{% endif %}
tap_action:
action: navigate
navigation_path: /cameras-view/live
# ---- Quick actions / scenes ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:flash
heading: Quick Actions
heading_style: title
- type: grid
columns: 4
square: true
cards:
- type: custom:mushroom-template-card
primary: Goodnight
icon: mdi:moon-waxing-crescent
icon_color: indigo
tap_action:
action: call-service
service: light.turn_off
data: {}
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
- light.bathroom_light_1
- light.bathroom_light_2
- light.bathroom_light_3
- light.bathroom_light_4
- light.bathroom_light_5
- light.bathroom_light_6
- type: custom:mushroom-template-card
primary: All Lights Off
icon: mdi:lightbulb-off
icon_color: grey
tap_action:
action: call-service
service: light.turn_off
data:
entity_id: all
- type: custom:mushroom-template-card
primary: Bedtime Dim
icon: mdi:bed
icon_color: purple
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 10
color_temp_kelvin: 2200
target:
entity_id:
- light.vish_bedroom_light_1
- light.vish_bedroom_light_2
- light.vish_bedroom_light_3
- type: custom:mushroom-template-card
primary: Movie Mode
icon: mdi:movie-open
icon_color: deep-orange
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 15
color_temp_kelvin: 2500
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
# ---- Homelab strip ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:server
heading: Homelab
heading_style: title
- type: custom:mushroom-chips-card
alignment: start
chips:
- type: entity
entity: sensor.sonarr_wanted
icon: mdi:television-classic
icon_color: blue
content_info: state
- type: entity
entity: sensor.radarr_missing
icon: mdi:filmstrip
icon_color: orange
content_info: state
- type: entity
entity: sensor.sabnzbd_speed
icon: mdi:download
icon_color: cyan
content_info: state
- type: entity
entity: sensor.bazarr_badges
icon: mdi:subtitles
icon_color: purple
content_info: state
- type: custom:mushroom-template-card
primary: Library Totals
secondary: >-
{{ states('sensor.sonarr_shows') }} series •
{{ states('sensor.radarr_movies_2') }} movies •
{{ states('sensor.audiobookshelf_ebooks') }} ebooks
icon: mdi:library-shelves
icon_color: green
tap_action:
action: navigate
navigation_path: /homelab-view/homelab

View File

@@ -0,0 +1,811 @@
title: Homelab
views:
# ========================================================
# TAB 1: OVERVIEW — calendar + kuma summary + quick launch
# ========================================================
- type: sections
title: Overview
path: homelab
icon: mdi:view-dashboard
max_columns: 3
sections:
# ---- Calendar (Baikal + Radarr) ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:calendar-month
heading: Calendar
heading_style: title
- type: custom:calendar-card-pro
entities:
- entity: calendar.vish
color: "#a78bfa"
- entity: calendar.radarr
color: "#f59e0b"
days_to_show: 14
max_events_to_show: 20
show_past_events: false
compact_mode: false
# ---- Kuma summary ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:heart-pulse
heading: Uptime Kuma
heading_style: title
- type: custom:mushroom-template-card
primary: >-
{{ states.sensor | selectattr('entity_id','match','^sensor\..*_status$') | list | count }}
secondary: Total Monitors
icon: mdi:pulse
icon_color: blue
tap_action:
action: url
url_path: http://100.77.151.40:3001
- type: custom:mushroom-template-card
primary: >-
{{ states.sensor | selectattr('entity_id','match','^sensor\..*_status$')
| rejectattr('state','eq','up')
| rejectattr('state','eq','unavailable')
| rejectattr('state','eq','unknown')
| list | count }}
secondary: Down / Degraded
icon: mdi:alert-circle
icon_color: red
- type: custom:mushroom-template-card
primary: >-
{% set rts = states.sensor | selectattr('entity_id','match','^sensor\..*_response_time$')
| map(attribute='state') | reject('in',['unavailable','unknown','none'])
| map('float',0) | reject('le',0) | list %}
{{ (rts | sum / (rts | count | max(1)) ) | round(0) }}
secondary: Avg Response (ms)
icon: mdi:speedometer
icon_color: green
# ---- Library totals ----
- type: grid
column_span: 3
cards:
- type: heading
icon: mdi:library
heading: Library Totals
heading_style: title
- type: grid
columns: 5
square: false
cards:
- type: tile
entity: sensor.atlantis_library_movies
name: Movies
icon: mdi:movie-open
- type: tile
entity: sensor.atlantis_library_tv_shows
name: TV Shows
icon: mdi:television-classic
- type: tile
entity: sensor.atlantis_library_anime
name: Anime
icon: mdi:sword-cross
- type: tile
entity: sensor.atlantis_library_music
name: Music
icon: mdi:music
- type: tile
entity: sensor.audiobookshelf_ebooks
name: Ebooks
icon: mdi:book-open-variant
# ---- Quick launch ----
- type: grid
column_span: 3
cards:
- type: heading
icon: mdi:rocket-launch
heading: Quick Launch
heading_style: title
- type: grid
columns: 6
square: true
cards:
- type: button
name: Plex
icon: mdi:plex
tap_action: {action: url, url_path: https://app.plex.tv}
- type: button
name: Sonarr
icon: mdi:television-classic
tap_action: {action: url, url_path: http://100.83.230.112:8989}
- type: button
name: Radarr
icon: mdi:filmstrip
tap_action: {action: url, url_path: http://100.83.230.112:7878}
- type: button
name: Bazarr
icon: mdi:subtitles
tap_action: {action: url, url_path: http://100.83.230.112:6767}
- type: button
name: Prowlarr
icon: mdi:magnify
tap_action: {action: url, url_path: http://100.83.230.112:9696}
- type: button
name: SABnzbd
icon: mdi:download
tap_action: {action: url, url_path: http://100.83.230.112:8080}
- type: button
name: LazyLib
icon: mdi:book-clock
tap_action: {action: url, url_path: http://100.83.230.112:5299}
- type: button
name: ABS
icon: mdi:headphones
tap_action: {action: url, url_path: http://100.83.230.112:13378}
- type: button
name: Portainer
icon: mdi:docker
tap_action: {action: url, url_path: https://pt.vish.gg}
- type: button
name: Gitea
icon: mdi:git
tap_action: {action: url, url_path: https://git.vish.gg}
- type: button
name: Homarr
icon: mdi:view-dashboard
tap_action: {action: url, url_path: https://homarr.vish.gg}
- type: button
name: Kuma
icon: mdi:heart-pulse
tap_action: {action: url, url_path: http://100.77.151.40:3001}
# ========================================================
# TAB 2: MEDIA — Plex, *arr, downloads, books
# ========================================================
- type: sections
title: Media
path: media
icon: mdi:play-circle
max_columns: 3
sections:
# ---- Plex ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:plex
heading: Plex
heading_style: title
- type: tile
entity: sensor.atlantis
name: Now Playing
icon: mdi:plex
tap_action:
action: more-info
- type: horizontal-stack
cards:
- type: tile
entity: sensor.atlantis_library_movies
name: Movies
icon: mdi:movie-open
- type: tile
entity: sensor.atlantis_library_tv_shows
name: TV
icon: mdi:television-classic
- type: horizontal-stack
cards:
- type: tile
entity: sensor.atlantis_library_anime
name: Anime
icon: mdi:sword-cross
- type: tile
entity: sensor.atlantis_library_music
name: Music
icon: mdi:music
# ---- Sonarr + Radarr ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:television-classic
heading: Sonarr + Radarr
heading_style: title
- type: horizontal-stack
cards:
- type: tile
entity: sensor.sonarr_queue_2
name: Sonarr Q
icon: mdi:television-classic
tap_action: {action: url, url_path: http://100.83.230.112:8989}
- type: tile
entity: sensor.radarr_queue_2
name: Radarr Q
icon: mdi:filmstrip
tap_action: {action: url, url_path: http://100.83.230.112:7878}
- type: horizontal-stack
cards:
- type: tile
entity: sensor.sonarr_shows
name: Shows
icon: mdi:television-box
- type: tile
entity: sensor.radarr_movies_2
name: Movies
icon: mdi:movie-open
- type: horizontal-stack
cards:
- type: tile
entity: sensor.sonarr_upcoming
name: Upcoming
icon: mdi:calendar-clock
- type: tile
entity: sensor.sonarr_wanted
name: Wanted
icon: mdi:television-off
- type: horizontal-stack
cards:
- type: tile
entity: sensor.bazarr_badges
name: Bazarr
icon: mdi:subtitles
tap_action: {action: url, url_path: http://100.83.230.112:6767}
- type: tile
entity: sensor.prowlarr_indexers
name: Prowlarr
icon: mdi:magnify
tap_action: {action: url, url_path: http://100.83.230.112:9696}
# ---- Books ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:book-open-page-variant
heading: Books & Audiobooks
heading_style: title
- type: tile
entity: sensor.audiobookshelf_ebooks
name: Ebooks
icon: mdi:book-open-variant
tap_action: {action: url, url_path: http://100.83.230.112:13378}
- type: tile
entity: sensor.audiobookshelf_audiobooks
name: Audiobooks
icon: mdi:headphones
- type: tile
entity: sensor.lazylibrarian_wanted_books
name: LazyLib Wanted
icon: mdi:book-clock
tap_action: {action: url, url_path: http://100.83.230.112:5299}
- type: tile
entity: sensor.lazylibrarian_version
name: LazyLib Version
icon: mdi:tag
# ---- Downloads (SABnzbd) ----
- type: grid
column_span: 3
cards:
- type: heading
icon: mdi:download
heading: Downloads — SABnzbd
heading_style: title
- type: grid
columns: 3
square: false
cards:
- type: tile
entity: sensor.sabnzbd_speed
name: Speed
icon: mdi:download-network
tap_action: {action: url, url_path: http://100.83.230.112:8080}
- type: tile
entity: sensor.sabnzbd_queue
name: Queue
icon: mdi:tray-full
- type: tile
entity: sensor.sabnzbd_status
name: Status
icon: mdi:information-outline
- type: entities
title: Details
show_header_toggle: false
entities:
- entity: sensor.sabnzbd_left_to_download
- entity: sensor.sabnzbd_queue_count
- entity: sensor.sabnzbd_free_disk_space
- entity: sensor.sabnzbd_daily_total
- entity: sensor.sabnzbd_weekly_total
- entity: sensor.sabnzbd_monthly_total
- type: horizontal-stack
cards:
- type: button
entity: button.sabnzbd_pause
name: Pause
icon: mdi:pause
tap_action:
action: call-service
service: button.press
target:
entity_id: button.sabnzbd_pause
- type: button
entity: button.sabnzbd_resume
name: Resume
icon: mdi:play
tap_action:
action: call-service
service: button.press
target:
entity_id: button.sabnzbd_resume
# ========================================================
# TAB 3: SERVERS — NASes + Proxmox + TrueNAS
# ========================================================
- type: sections
title: Servers
path: servers
icon: mdi:server-network
max_columns: 3
sections:
# ---- Atlantis NAS (Synology) ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:nas
heading: Atlantis NAS (Synology)
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.atlantis_cpu_utilization_total
name: CPU
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 60, red: 85}
- type: gauge
entity: sensor.atlantis_memory_usage_real
name: Memory
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 70, red: 90}
- type: gauge
entity: sensor.atlantis_temperature
name: Temp °F
min: 60
max: 180
severity: {green: 60, yellow: 140, red: 160}
- type: entities
title: Volumes
show_header_toggle: false
entities:
- {entity: sensor.atlantis_volume_1_volume_used, name: Volume 1}
- {entity: sensor.atlantis_volume_2_volume_used, name: Volume 2}
- {entity: sensor.atlantis_volume_3_volume_used, name: Volume 3}
- {entity: sensor.atlantis_volume_1_used_space, name: V1 Used (TB)}
- {entity: sensor.atlantis_volume_1_status, name: V1 Health}
- type: horizontal-stack
cards:
- type: tile
entity: sensor.atlantis_download_throughput
name: ↓ Download
icon: mdi:download
- type: tile
entity: sensor.atlantis_upload_throughput
name: ↑ Upload
icon: mdi:upload
- type: entities
title: Drive Temperatures (°F)
show_header_toggle: false
entities:
- {entity: sensor.atlantis_drive_1_temperature, name: Drive 1}
- {entity: sensor.atlantis_drive_2_temperature, name: Drive 2}
- {entity: sensor.atlantis_drive_3_temperature, name: Drive 3}
- {entity: sensor.atlantis_drive_4_temperature, name: Drive 4}
- {entity: sensor.atlantis_drive_5_temperature, name: Drive 5}
- {entity: sensor.atlantis_drive_6_temperature, name: Drive 6}
- {entity: sensor.atlantis_drive_7_temperature, name: Drive 7}
- {entity: sensor.atlantis_drive_8_temperature, name: Drive 8}
- {entity: sensor.atlantis_m_2_drive_1_temperature, name: NVMe 1}
- {entity: sensor.atlantis_m_2_drive_2_temperature, name: NVMe 2}
- type: conditional
conditions:
- condition: state
entity: update.atlantis_dsm_update
state: "on"
card:
type: tile
entity: update.atlantis_dsm_update
name: ⚠ DSM Update Available
# ---- Calypso NAS (compact) ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:nas
heading: Calypso NAS
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.calypso_cpu_utilization_total
name: CPU
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 60, red: 85}
- type: gauge
entity: sensor.calypso_memory_usage_real
name: Mem
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 70, red: 90}
- type: entities
title: Volume & Drives
show_header_toggle: false
entities:
- {entity: sensor.calypso_volume_1_volume_used, name: Volume 1 %}
- {entity: sensor.calypso_volume_1_used_space, name: V1 Used (TB)}
- {entity: sensor.calypso_volume_1_status, name: V1 Health}
- {entity: sensor.calypso_drive_1_temperature, name: Drive 1 Temp}
- {entity: sensor.calypso_drive_2_temperature, name: Drive 2 Temp}
- {entity: sensor.calypso_m_2_drive_1_temperature, name: NVMe 1 Temp}
- {entity: sensor.calypso_m_2_drive_2_temperature, name: NVMe 2 Temp}
- {entity: sensor.calypso_temperature, name: System Temp}
- {entity: binary_sensor.calypso_security_status, name: Security}
- type: conditional
conditions:
- condition: state
entity: update.calypso_dsm_update
state: "on"
card:
type: tile
entity: update.calypso_dsm_update
name: ⚠ Calypso DSM Update
# ---- Setillo NAS (compact) ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:nas
heading: Setillo NAS
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.setillo_cpu_utilization_total
name: CPU
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 60, red: 85}
- type: gauge
entity: sensor.setillo_memory_usage_real
name: Mem
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 70, red: 90}
- type: entities
title: Volume & Drives
show_header_toggle: false
entities:
- {entity: sensor.setillo_volume_1_volume_used, name: Volume 1 %}
- {entity: sensor.setillo_volume_1_used_space, name: V1 Used (TB)}
- {entity: sensor.setillo_volume_1_status, name: V1 Health}
- {entity: sensor.setillo_drive_1_temperature, name: Drive 1 Temp}
- {entity: sensor.setillo_drive_2_temperature, name: Drive 2 Temp}
- {entity: sensor.setillo_temperature, name: System Temp}
- {entity: binary_sensor.setillo_security_status, name: Security}
- {entity: switch.setillo_surveillance_station_home_mode, name: Surveillance Home Mode}
- type: conditional
conditions:
- condition: state
entity: update.setillo_dsm_update
state: "on"
card:
type: tile
entity: update.setillo_dsm_update
name: ⚠ Setillo DSM Update
# ---- Guava TrueNAS ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:harddisk
heading: Guava (TrueNAS Scale)
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.guava_system_cpu_usage
name: CPU
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 60, red: 85}
- type: gauge
entity: sensor.guava_system_memory_usage
name: Memory
min: 0
max: 100
unit: "%"
severity: {green: 0, yellow: 70, red: 90}
- type: tile
entity: sensor.guava_system_temperature
name: Temp
icon: mdi:thermometer
- type: entities
title: Pool Health
show_header_toggle: false
entities:
- {entity: binary_sensor.guava_system_data_healthy, name: Data Pool}
- {entity: binary_sensor.guava_system_boot_pool_healthy, name: Boot Pool}
- {entity: sensor.guava_system_data_free, name: Data Pool Free}
- {entity: sensor.guava_system_boot_pool_free, name: Boot Pool Free}
- {entity: sensor.guava_system_arc_size, name: ZFS ARC Size}
- {entity: sensor.guava_system_uptime, name: Uptime}
- type: entities
title: Services + VM
show_header_toggle: false
entities:
- {entity: binary_sensor.guava_services_nfs, name: NFS}
- {entity: binary_sensor.guava_services_cifs, name: SMB/CIFS}
- {entity: binary_sensor.guava_services_ssh, name: SSH}
- {entity: binary_sensor.guava_services_smartd, name: SMART Daemon}
- {entity: binary_sensor.guava_services_snmp, name: SNMP}
- {entity: binary_sensor.guava_services_ups, name: UPS}
- {entity: binary_sensor.guava_vms_proton_bridge, name: Proton Bridge VM}
- type: entities
title: Disks (SMART)
show_header_toggle: false
entities:
- {entity: sensor.guava_disks_nvme0n1, name: NVMe (boot)}
- {entity: sensor.guava_disks_sda, name: Disk sda}
- {entity: sensor.guava_disks_sdb, name: Disk sdb}
- type: conditional
conditions:
- condition: state
entity: update.guava_system
state: "on"
card:
type: tile
entity: update.guava_system
name: ⚠ Guava TrueNAS Update
# ---- Proxmox VE ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:server-network
heading: Proxmox VE
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.pve_cpu_usage
name: CPU
min: 0
max: 100
severity: {green: 0, yellow: 60, red: 85}
- type: gauge
entity: sensor.pve_memory_usage_percentage
name: Mem
min: 0
max: 100
severity: {green: 0, yellow: 70, red: 90}
- type: entities
title: PVE Host
show_header_toggle: false
entities:
- {entity: binary_sensor.pve_status, name: Status}
- {entity: sensor.pve_uptime, name: Uptime}
- {entity: sensor.pve_memory_usage, name: Memory Used}
- {entity: sensor.pve_disk_usage, name: Disk Used}
- {entity: binary_sensor.pve_backup_status, name: Backup Status}
- {entity: sensor.pve_last_backup, name: Last Backup}
- type: horizontal-stack
cards:
- type: button
entity: button.pve_start_all
name: Start All
icon: mdi:play-circle
tap_action:
action: call-service
service: button.press
target:
entity_id: button.pve_start_all
- type: button
entity: button.pve_stop_all
name: Stop All
icon: mdi:stop-circle
tap_action:
action: call-service
service: button.press
target:
entity_id: button.pve_stop_all
- type: button
entity: button.pve_restart
name: Restart
icon: mdi:restart
tap_action:
action: call-service
service: button.press
target:
entity_id: button.pve_restart
confirmation:
text: "Restart PVE host?"
# ========================================================
# TAB 4: MONITORING — Kuma detail + service pings
# ========================================================
- type: sections
title: Monitoring
path: monitoring
icon: mdi:heart-pulse
max_columns: 2
sections:
# ---- Summary chips (same as Overview) ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:heart-pulse
heading: Uptime Kuma — Live
heading_style: title
- type: horizontal-stack
cards:
- type: custom:mushroom-template-card
primary: >-
{{ states.sensor | selectattr('entity_id','match','^sensor\..*_status$') | list | count }}
secondary: Total Monitors
icon: mdi:pulse
icon_color: blue
tap_action:
action: url
url_path: http://100.77.151.40:3001
- type: custom:mushroom-template-card
primary: >-
{{ states.sensor | selectattr('entity_id','match','^sensor\..*_status$')
| rejectattr('state','eq','up')
| rejectattr('state','eq','unavailable')
| rejectattr('state','eq','unknown')
| list | count }}
secondary: Down
icon: mdi:alert-circle
icon_color: red
- type: custom:mushroom-template-card
primary: >-
{% set rts = states.sensor | selectattr('entity_id','match','^sensor\..*_response_time$')
| map(attribute='state') | reject('in',['unavailable','unknown','none'])
| map('float',0) | reject('le',0) | list %}
{{ (rts | sum / (rts | count | max(1)) ) | round(0) }}
secondary: Avg Response (ms)
icon: mdi:speedometer
icon_color: green
- type: custom:mushroom-template-card
primary: >-
{{ states.sensor | selectattr('entity_id','match','^sensor\..*_certificate_expiry$')
| map(attribute='state') | map('int',999) | reject('ge',30) | list | count }}
secondary: Certs < 30 days
icon: mdi:certificate
icon_color: orange
# ---- Core infra ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:server
heading: Core Infrastructure
heading_style: subtitle
- type: entities
show_header_toggle: false
entities:
- {entity: sensor.atlantis_status, name: Atlantis (NAS)}
- {entity: sensor.proxmox_nuc_status, name: Proxmox NUC}
- {entity: sensor.home_assistant_status, name: Home Assistant}
- {entity: sensor.authentik_status, name: Authentik SSO}
- {entity: sensor.headscale_status, name: Headscale}
- {entity: sensor.crowdsec_lapi_status, name: CrowdSec}
- {entity: sensor.nginx_proxy_manager_status, name: NPM}
- {entity: sensor.atl_portainer_status, name: Portainer}
# ---- Apps ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:application
heading: Apps & Services
heading_style: subtitle
- type: entities
show_header_toggle: false
entities:
- {entity: sensor.jellyfin_status, name: Jellyfin}
- {entity: sensor.ollama_status, name: Ollama}
- {entity: sensor.gitea_status, name: Gitea}
- {entity: sensor.grafana_status, name: Grafana}
- {entity: sensor.homarr_status, name: Homarr}
- {entity: sensor.bitwarden_status, name: Bitwarden}
- {entity: sensor.paperless_ngx_status, name: Paperless}
- {entity: sensor.immich_status, name: Immich}
- {entity: sensor.seafile_status, name: Seafile}
# ---- Certificate expiry watch ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:certificate
heading: SSL Certificates (sorted by expiry)
heading_style: subtitle
- type: entities
show_header_toggle: false
entities:
- {entity: sensor.crista_s_website_certificate_expiry, name: crista.love}
- {entity: sensor.gitea_certificate_expiry, name: gitea}
- {entity: sensor.homarr_certificate_expiry, name: homarr}
- {entity: sensor.authentik_certificate_expiry, name: authentik}
- {entity: sensor.headscale_certificate_expiry, name: headscale}
- {entity: sensor.ollama_certificate_expiry, name: ollama}
- {entity: sensor.grafana_certificate_expiry, name: grafana}
- {entity: sensor.nginx_proxy_manager_certificate_expiry, name: npm}
- {entity: sensor.crowdsec_lapi_certificate_expiry, name: crowdsec}
- {entity: sensor.matrix_certificate_expiry, name: matrix}

View File

@@ -0,0 +1,10 @@
title: Homelab Web
views:
- title: Dashboard
path: web
type: panel
icon: mdi:home-analytics
cards:
- type: iframe
url: http://homelab.tail.vish.gg:3100
aspect_ratio: "100%"

View File

@@ -0,0 +1,161 @@
title: Kitchen
views:
- type: sections
title: Kitchen
path: kitchen
icon: mdi:stove
max_columns: 3
sections:
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:stove
heading: Kitchen
heading_style: title
- type: tile
entity: light.kitchen_above_sink
name: Above Sink
icon: mdi:ceiling-light
vertical: true
features_position: bottom
features:
- type: light-brightness
tap_action:
action: toggle
- type: grid
columns: 2
square: false
cards:
- type: tile
entity: light.kitchen_light_1
name: Light 1
icon: mdi:ceiling-light
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.kitchen_light_2
name: Light 2
icon: mdi:ceiling-light
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.kitchen_light_3
name: Light 3
icon: mdi:ceiling-light
vertical: true
features:
- type: light-brightness
- type: tile
entity: light.kitchen_light_4
name: Light 4
icon: mdi:ceiling-light
vertical: true
features:
- type: light-brightness
- type: horizontal-stack
cards:
- type: button
name: All On
icon: mdi:lightbulb-group
tap_action:
action: call-service
service: light.turn_on
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
- type: button
name: All Off
icon: mdi:lightbulb-group-off
tap_action:
action: call-service
service: light.turn_off
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
- type: button
name: Cooking
icon: mdi:chef-hat
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 100
color_temp_kelvin: 4000
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
- type: button
name: Dim
icon: mdi:weather-night
tap_action:
action: call-service
service: light.turn_on
data:
brightness_pct: 20
color_temp_kelvin: 2700
target:
entity_id:
- light.kitchen_above_sink
- light.kitchen_light_1
- light.kitchen_light_2
- light.kitchen_light_3
- light.kitchen_light_4
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:chart-line
heading: Health
heading_style: title
- type: entities
title: Bulb Status
show_header_toggle: false
entities:
- entity: binary_sensor.kitchen_above_sink_cloud_connection
name: Above Sink - Cloud
- entity: binary_sensor.kitchen_above_sink_overheated
name: Above Sink - Overheat
- entity: binary_sensor.kitchen_light_1_cloud_connection
name: Light 1 - Cloud
- entity: binary_sensor.kitchen_light_2_cloud_connection
name: Light 2 - Cloud
- entity: binary_sensor.kitchen_light_3_cloud_connection
name: Light 3 - Cloud
- entity: binary_sensor.kitchen_light_4_cloud_connection
name: Light 4 - Cloud
- type: entities
title: Signal Strength
show_header_toggle: false
entities:
- entity: sensor.kitchen_above_sink_signal_level
name: Above Sink
- entity: sensor.kitchen_light_1_signal_level
name: Light 1
- entity: sensor.kitchen_light_2_signal_level
name: Light 2
- entity: sensor.kitchen_light_3_signal_level
name: Light 3
- entity: sensor.kitchen_light_4_signal_level
name: Light 4

View File

@@ -0,0 +1,227 @@
title: Living Room
views:
- type: sections
title: Living Room
path: living-room
icon: mdi:sofa
max_columns: 3
sections:
# ---- Weather ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:weather-partly-cloudy
heading: Weather
heading_style: title
- type: weather-forecast
entity: weather.forecast_home
forecast_type: daily
show_current: true
show_forecast: true
- type: weather-forecast
entity: weather.forecast_home
forecast_type: hourly
show_current: false
show_forecast: true
# ---- Media ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:television
heading: Media
heading_style: title
- type: media-control
entity: media_player.tv_living_room
- type: horizontal-stack
cards:
- type: button
icon: mdi:spotify
name: Spotify
tap_action:
action: navigate
navigation_path: /bedroom-yaml/bedroom
- type: button
icon: mdi:cast
name: Cast Hub
tap_action:
action: more-info
entity: media_player.tv_living_room
- type: button
icon: mdi:remote-tv
name: TV Power
tap_action:
action: call-service
service: media_player.toggle
target:
entity_id: media_player.tv_living_room
- type: picture-glance
title: Living Room Camera
camera_view: live
camera_image: camera.192_168_69_116
entities: []
# ---- Security / Hub ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:security
heading: Hub & Alarm
heading_style: title
- type: tile
entity: siren.hub_wired
name: Hub Siren
icon: mdi:alarm-light
vertical: true
tap_action:
action: toggle
- type: entities
title: Hub
show_header_toggle: false
entities:
- entity: switch.hub_wired_led
name: Hub LED
- entity: select.hub_wired_alarm_sound
name: Alarm Sound
# ---- Network health ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:wifi
heading: Network
heading_style: title
- type: horizontal-stack
cards:
- type: gauge
entity: sensor.speedtest_download
name: Download
unit: Mbps
min: 0
max: 1000
severity:
green: 300
yellow: 100
red: 0
- type: gauge
entity: sensor.speedtest_upload
name: Upload
unit: Mbps
min: 0
max: 500
severity:
green: 100
yellow: 30
red: 0
- type: gauge
entity: sensor.speedtest_ping
name: Ping
unit: ms
min: 0
max: 100
severity:
green: 0
yellow: 30
red: 60
needle: true
- type: history-graph
title: Download / Upload (24h)
hours_to_show: 24
entities:
- entity: sensor.speedtest_download
- entity: sensor.speedtest_upload
- type: entities
title: Deco Mesh
show_header_toggle: false
entities:
- entity: binary_sensor.living_room_deco_internet_online
name: Living Room Deco - Internet
- entity: binary_sensor.living_room_deco_deco_online
name: Living Room Deco - Online
- entity: binary_sensor.main_bedroom_deco_deco_online
name: Main Bedroom Deco
- entity: binary_sensor.kevins_room_deco_deco_online
name: Kevin's Room Deco
# ---- Electricity (PG&E) ----
- type: grid
column_span: 2
cards:
- type: heading
icon: mdi:flash
heading: Electricity (PG&E)
heading_style: title
- type: horizontal-stack
cards:
- type: tile
entity: sensor.current_bill_electric_usage_to_date
name: Usage So Far
icon: mdi:meter-electric
- type: tile
entity: sensor.current_bill_electric_forecasted_usage
name: Forecasted Usage
icon: mdi:chart-line-variant
- type: tile
entity: sensor.typical_monthly_electric_usage
name: Typical
icon: mdi:calendar-month
- type: horizontal-stack
cards:
- type: tile
entity: sensor.current_bill_electric_cost_to_date
name: Cost So Far
icon: mdi:currency-usd
- type: tile
entity: sensor.current_bill_electric_forecasted_cost
name: Forecast
icon: mdi:cash-multiple
- type: tile
entity: sensor.typical_monthly_electric_cost
name: Typical Cost
icon: mdi:cash-clock
# ---- AdGuard ----
- type: grid
column_span: 1
cards:
- type: heading
icon: mdi:shield-check
heading: AdGuard
heading_style: title
- type: tile
entity: switch.adguard_home_protection
name: Protection
icon: mdi:shield
- type: entities
title: Today
show_header_toggle: false
entities:
- entity: sensor.adguard_home_dns_queries
name: DNS Queries
- entity: sensor.adguard_home_dns_queries_blocked
name: Blocked
- entity: sensor.adguard_home_dns_queries_blocked_ratio
name: Block Ratio
- entity: sensor.adguard_home_safe_browsing_blocked
name: Safe Browsing
- entity: sensor.adguard_home_rules_count
name: Rules

View File

@@ -0,0 +1,14 @@
# Home Assistant secrets — referenced as !secret <key> in configuration
# Private repo: committed directly (REDACTED in public mirror).
# Homelab arr-suite (Atlantis, Tailscale 100.83.230.112)
sonarr_api_key: "REDACTED_API_KEY"
radarr_api_key: "REDACTED_API_KEY"
sabnzbd_api_key: "REDACTED_API_KEY"
prowlarr_api_key: "REDACTED_API_KEY"
bazarr_api_key: "REDACTED_API_KEY"
lazylibrarian_api_key: "REDACTED_LL_API_KEY"
audiobookshelf_api_key: "Bearer REDACTED_ABS_API_TOKEN"
# Plex (local on this NUC)
plex_token: "REDACTED_TOKEN"

View File

@@ -0,0 +1,82 @@
# REST sensors for homelab services that don't have native HA integrations.
# Sonarr / Radarr / SABnzbd / Plex now use native HA integrations instead —
# their REST sensor definitions were removed 2026-04-19.
#
# Service ports (Atlantis via Tailscale 100.83.230.112):
# bazarr 6767 /api
# lazylib 5299 /api
# abs 13378 /api
# prowlarr 9696 /api/v1
# -------- Prowlarr --------
- platform: rest
name: Prowlarr Indexers
unique_id: homelab_rest_prowlarr_indexers
resource: "http://100.83.230.112:9696/api/v1/indexer"
headers:
X-Api-Key: !secret prowlarr_api_key
value_template: "{{ value_json | length }}"
unit_of_measurement: indexers
scan_interval: 3600
# -------- Bazarr --------
- platform: rest
name: Bazarr Badges
unique_id: homelab_rest_bazarr_badges
resource: "http://100.83.230.112:6767/api/badges"
headers:
X-Api-Key: !secret bazarr_api_key
value_template: "{{ (value_json.missing_subtitles_movies | default(0)) + (value_json.missing_subtitles_episodes | default(0)) }}"
unit_of_measurement: missing
json_attributes:
- missing_subtitles_episodes
- missing_subtitles_movies
- throttled_providers
scan_interval: 600
# -------- LazyLibrarian --------
- platform: rest
name: LazyLibrarian Version
unique_id: homelab_rest_lazylibrarian_version
resource: "http://100.83.230.112:5299/api?apikey=REDACTED_LL_API_KEY&cmd=getVersion"
value_template: "{{ value_json.current_version | default('?') }}"
scan_interval: 86400
- platform: rest
name: LazyLibrarian Wanted Books
unique_id: homelab_rest_lazylibrarian_wanted_books
resource: "http://100.83.230.112:5299/api?apikey=REDACTED_LL_API_KEY&cmd=getWanted"
value_template: "{{ value_json.data | length if value_json.data is defined else 0 }}"
unit_of_measurement: books
scan_interval: 600
# -------- Audiobookshelf --------
- platform: rest
name: Audiobookshelf Libraries
unique_id: homelab_rest_audiobookshelf_libraries
resource: "http://100.83.230.112:13378/api/libraries"
headers:
Authorization: !secret audiobookshelf_api_key
value_template: "{{ value_json.libraries | length }}"
unit_of_measurement: libraries
scan_interval: 3600
- platform: rest
name: Audiobookshelf Ebooks
unique_id: homelab_rest_audiobookshelf_ebooks
resource: "http://100.83.230.112:13378/api/libraries/5af23ed3-f69d-479b-88bc-1c4911c99d2d/items?limit=1"
headers:
Authorization: !secret audiobookshelf_api_key
value_template: "{{ value_json.total | default(0) }}"
unit_of_measurement: items
scan_interval: 3600
- platform: rest
name: Audiobookshelf Audiobooks
unique_id: homelab_rest_audiobookshelf_audiobooks
resource: "http://100.83.230.112:13378/api/libraries/d36776eb-fe81-467f-8fee-19435ee2827b/items?limit=1"
headers:
Authorization: !secret audiobookshelf_api_key
value_template: "{{ value_json.total | default(0) }}"
unit_of_measurement: items
scan_interval: 3600

View File

@@ -0,0 +1,143 @@
cyberpunk:
# ===== font (angular, tech) =====
primary-font-family: '"Rajdhani", "Orbitron", "Share Tech Mono", "Exo 2", -apple-system, sans-serif'
paper-font-common-base_-_font-family: '"Rajdhani", "Orbitron", "Exo 2", -apple-system, sans-serif'
paper-font-body1_-_font-family: '"Rajdhani", "Exo 2", -apple-system, sans-serif'
paper-font-subhead_-_font-family: '"Rajdhani", "Exo 2", -apple-system, sans-serif'
paper-font-headline_-_font-family: '"Orbitron", "Rajdhani", "Exo 2", -apple-system, sans-serif'
paper-font-title_-_font-family: '"Orbitron", "Rajdhani", "Exo 2", -apple-system, sans-serif'
ha-card-header-font-family: '"Orbitron", "Rajdhani", "Exo 2", -apple-system, sans-serif'
# ===== background (neon grid, night city) =====
lovelace-background: 'radial-gradient(1400px 900px at 10% 15%, rgba(255, 45, 149, 0.28) 0%, transparent 55%), radial-gradient(1200px 800px at 90% 85%, rgba(0, 240, 255, 0.25) 0%, transparent 50%), radial-gradient(800px 600px at 50% 50%, rgba(255, 220, 0, 0.08) 0%, transparent 60%), linear-gradient(135deg, #0a0014 0%, #05010d 50%, #000005 100%)'
primary-background-color: '#05010d'
secondary-background-color: '#0d0220'
app-header-background-color: 'rgba(5, 1, 13, 0.85)'
app-header-text-color: '#ffeb00'
sidebar-background-color: 'rgba(10, 0, 20, 0.92)'
sidebar-text-color: '#d0d0e8'
sidebar-selected-text-color: '#00f0ff'
sidebar-selected-background-color: 'rgba(255, 45, 149, 0.18)'
sidebar-icon-color: '#9a8ec0'
sidebar-selected-icon-color: '#00f0ff'
# ===== card (neon panel) =====
ha-card-background: 'rgba(15, 0, 30, 0.92)'
card-background-color: 'rgba(15, 0, 30, 0.92)'
ha-card-border-radius: '4px'
ha-card-border-width: '1px'
ha-card-border-color: 'rgba(255, 45, 149, 0.35)'
ha-card-box-shadow: '0 0 20px rgba(255, 45, 149, 0.25), 0 0 40px rgba(0, 240, 255, 0.10), inset 0 1px 0 rgba(255, 45, 149, 0.15)'
ha-card-header-color: '#ffeb00'
# ===== more-info dialog / popup (opaque, readable) =====
mdc-theme-surface: '#0f001e'
mdc-dialog-scrim-color: 'rgba(0, 0, 0, 0.80)'
dialog-backdrop-filter: 'blur(10px)'
ha-dialog-surface-background: '#0f001e'
ha-dialog-border-radius: '4px'
# ===== text =====
primary-text-color: '#e8e8f8'
secondary-text-color: '#8a89c0'
text-primary-color: '#ffffff'
disabled-text-color: '#4a4870'
# ===== accents =====
primary-color: '#ff2d95'
accent-color: '#00f0ff'
light-primary-color: '#ff7ec2'
dark-primary-color: '#c41175'
label-badge-background-color: 'rgba(15, 0, 30, 0.90)'
label-badge-text-color: '#ffeb00'
label-badge-red: '#ff2d95'
label-badge-green: '#00ff9c'
label-badge-blue: '#00f0ff'
label-badge-yellow: '#ffeb00'
label-badge-grey: '#8a89c0'
# ===== state colors =====
state-icon-color: '#00f0ff'
state-icon-active-color: '#ff2d95'
state-icon-unavailable-color: '#4a4870'
paper-item-icon-color: '#9a8ec0'
paper-item-icon-active-color: '#ff2d95'
# ===== domain states =====
state-binary-sensor-active-color: '#00ff9c'
state-light-active-color: '#ffeb00'
state-switch-active-color: '#00f0ff'
state-fan-active-color: '#00f0ff'
state-media-player-active-color: '#ff2d95'
state-person-home-color: '#00ff9c'
state-person-not_home-color: '#8a89c0'
# ===== toggles =====
switch-checked-color: '#ff2d95'
switch-checked-button-color: '#ff7ec2'
switch-checked-track-color: 'rgba(255, 45, 149, 0.45)'
switch-unchecked-button-color: '#4a4870'
switch-unchecked-track-color: 'rgba(74, 72, 112, 0.45)'
# ===== sliders =====
paper-slider-knob-color: '#ff2d95'
paper-slider-knob-start-color: '#ff2d95'
paper-slider-pin-color: '#ff2d95'
paper-slider-active-color: '#00f0ff'
paper-slider-container-color: 'rgba(255, 45, 149, 0.30)'
paper-slider-secondary-color: '#c41175'
# ===== dividers / outlines =====
divider-color: 'rgba(255, 45, 149, 0.18)'
outline-color: 'rgba(0, 240, 255, 0.20)'
# ===== input elements =====
input-background-color: 'rgba(15, 0, 30, 0.80)'
input-fill-color: 'rgba(15, 0, 30, 0.80)'
input-ink-color: '#e8e8f8'
input-label-ink-color: '#8a89c0'
input-idle-line-color: 'rgba(255, 45, 149, 0.25)'
input-hover-line-color: '#ff2d95'
input-focused-line-color: '#00f0ff'
# ===== MDC select + text-field (dropdowns inside cards/dialogs) =====
mdc-select-fill-color: 'rgba(15, 0, 30, 0.90)'
mdc-select-ink-color: '#e8e8f8'
mdc-select-label-ink-color: '#8a89c0'
mdc-select-dropdown-icon-color: '#00f0ff'
mdc-select-idle-line-color: 'rgba(255, 45, 149, 0.35)'
mdc-select-hover-line-color: '#ff2d95'
mdc-select-focused-label-color: '#00f0ff'
mdc-text-field-fill-color: 'rgba(15, 0, 30, 0.90)'
mdc-text-field-ink-color: '#e8e8f8'
mdc-text-field-label-ink-color: '#8a89c0'
mdc-text-field-idle-line-color: 'rgba(255, 45, 149, 0.35)'
mdc-text-field-hover-line-color: '#ff2d95'
mdc-text-field-focused-label-color: '#00f0ff'
mdc-text-field-disabled-fill-color: 'rgba(15, 0, 30, 0.60)'
mdc-text-field-disabled-ink-color: '#8a89c0'
mdc-filled-text-field-container-color: 'rgba(15, 0, 30, 0.90)'
mdc-filled-text-field-label-text-color: '#8a89c0'
mdc-filled-text-field-input-text-color: '#e8e8f8'
ha-textfield-background: 'rgba(15, 0, 30, 0.90)'
card-mod-theme: cyberpunk
card-mod-root: |
ha-voice-command-dialog $ ha-textfield {
--mdc-text-field-fill-color: rgba(15, 0, 30, 0.95) !important;
--mdc-text-field-ink-color: #e8e8f8 !important;
--mdc-text-field-label-ink-color: #8a89c0 !important;
}
ha-voice-command-dialog $ ha-textfield $ .mdc-text-field__input {
color: #e8e8f8 !important;
}
# ===== buttons =====
mdc-theme-primary: '#ff2d95'
mdc-theme-secondary: '#00f0ff'
mdc-theme-on-primary: '#ffffff'
mdc-theme-on-secondary: '#000000'
# ===== tables =====
table-row-background-color: 'transparent'
table-row-alternative-background-color: 'rgba(255, 45, 149, 0.04)'

View File

@@ -0,0 +1,154 @@
glass_exo:
# ===== font =====
primary-font-family: '"Exo 2", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
paper-font-common-base_-_font-family: '"Exo 2", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
paper-font-body1_-_font-family: '"Exo 2", "SF Pro Display", -apple-system, sans-serif'
paper-font-subhead_-_font-family: '"Exo 2", "SF Pro Display", -apple-system, sans-serif'
paper-font-headline_-_font-family: '"Exo 2", "SF Pro Display", -apple-system, sans-serif'
paper-font-title_-_font-family: '"Exo 2", "SF Pro Display", -apple-system, sans-serif'
ha-card-header-font-family: '"Exo 2", "SF Pro Display", -apple-system, sans-serif'
paper-font-common-base_-_-webkit-font-smoothing: 'antialiased'
# ===== background (gradient + soft glow) =====
lovelace-background: 'radial-gradient(1200px 800px at 15% 10%, rgba(99, 102, 241, 0.20) 0%, transparent 60%), radial-gradient(1000px 700px at 85% 90%, rgba(236, 72, 153, 0.18) 0%, transparent 55%), linear-gradient(135deg, #0b0f1a 0%, #0a0a14 45%, #060610 100%)'
primary-background-color: '#0a0a14'
secondary-background-color: '#10121c'
app-header-background-color: 'rgba(10, 10, 20, 0.65)'
app-header-text-color: '#e7e9f4'
sidebar-background-color: 'rgba(10, 10, 20, 0.85)'
sidebar-text-color: '#c7c9dc'
sidebar-selected-text-color: '#a78bfa'
sidebar-selected-background-color: 'rgba(167, 139, 250, 0.12)'
sidebar-icon-color: '#8a8db0'
sidebar-selected-icon-color: '#a78bfa'
# ===== card (glass panel) =====
ha-card-background: 'rgba(22, 24, 40, 0.88)'
card-background-color: 'rgba(22, 24, 40, 0.88)'
ha-card-border-radius: '18px'
ha-card-border-width: '1px'
ha-card-border-color: 'rgba(255, 255, 255, 0.08)'
ha-card-box-shadow: '0 8px 32px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.04)'
ha-card-header-color: '#e7e9f4'
# ===== more-info dialog / popup (opaque, readable) =====
mdc-theme-surface: '#161828'
mdc-dialog-scrim-color: 'rgba(0, 0, 0, 0.78)'
dialog-backdrop-filter: 'blur(12px)'
ha-dialog-surface-background: '#161828'
ha-dialog-border-radius: '20px'
# ===== text =====
primary-text-color: '#e7e9f4'
secondary-text-color: '#9a9cb8'
text-primary-color: '#ffffff'
disabled-text-color: '#5b5e78'
# ===== accents =====
primary-color: '#a78bfa'
accent-color: '#f472b6'
light-primary-color: '#c4b5fd'
dark-primary-color: '#7c3aed'
label-badge-background-color: 'rgba(22, 24, 40, 0.75)'
label-badge-text-color: '#e7e9f4'
label-badge-red: '#f87171'
label-badge-green: '#4ade80'
label-badge-blue: '#60a5fa'
label-badge-yellow: '#fbbf24'
label-badge-grey: '#9ca3af'
# ===== state colors =====
state-icon-color: '#a78bfa'
state-icon-active-color: '#f472b6'
state-icon-unavailable-color: '#6b7280'
paper-item-icon-color: '#9a9cb8'
paper-item-icon-active-color: '#f472b6'
# ===== domain states =====
state-binary-sensor-active-color: '#4ade80'
state-light-active-color: '#fbbf24'
state-switch-active-color: '#60a5fa'
state-fan-active-color: '#38bdf8'
state-climate-cooling-color: '#60a5fa'
state-climate-heating-color: '#f87171'
state-media-player-active-color: '#f472b6'
state-person-home-color: '#4ade80'
state-person-not_home-color: '#9ca3af'
# ===== toggles / switches =====
switch-checked-color: '#a78bfa'
switch-checked-button-color: '#c4b5fd'
switch-checked-track-color: 'rgba(167, 139, 250, 0.4)'
switch-unchecked-button-color: '#6b7280'
switch-unchecked-track-color: 'rgba(107, 114, 128, 0.4)'
# ===== sliders =====
paper-slider-knob-color: '#a78bfa'
paper-slider-knob-start-color: '#a78bfa'
paper-slider-pin-color: '#a78bfa'
paper-slider-active-color: '#a78bfa'
paper-slider-container-color: 'rgba(167, 139, 250, 0.25)'
paper-slider-secondary-color: '#7c3aed'
# ===== dividers / outlines =====
divider-color: 'rgba(255, 255, 255, 0.08)'
outline-color: 'rgba(255, 255, 255, 0.10)'
# ===== input elements =====
input-background-color: 'rgba(22, 24, 40, 0.70)'
input-fill-color: 'rgba(22, 24, 40, 0.70)'
input-ink-color: '#e7e9f4'
input-label-ink-color: '#9a9cb8'
input-idle-line-color: 'rgba(255, 255, 255, 0.12)'
input-hover-line-color: '#a78bfa'
input-focused-line-color: '#a78bfa'
# ===== MDC select + text-field (dropdowns inside cards/dialogs) =====
mdc-select-fill-color: 'rgba(22, 24, 40, 0.85)'
mdc-select-ink-color: '#e7e9f4'
mdc-select-label-ink-color: '#9a9cb8'
mdc-select-dropdown-icon-color: '#a78bfa'
mdc-select-idle-line-color: 'rgba(255, 255, 255, 0.18)'
mdc-select-hover-line-color: '#a78bfa'
mdc-select-focused-label-color: '#a78bfa'
mdc-text-field-fill-color: 'rgba(22, 24, 40, 0.85)'
mdc-text-field-ink-color: '#e7e9f4'
mdc-text-field-label-ink-color: '#9a9cb8'
mdc-text-field-idle-line-color: 'rgba(255, 255, 255, 0.18)'
mdc-text-field-hover-line-color: '#a78bfa'
mdc-text-field-focused-label-color: '#a78bfa'
mdc-text-field-disabled-fill-color: 'rgba(22, 24, 40, 0.60)'
mdc-text-field-disabled-ink-color: '#9a9cb8'
mdc-text-field-outlined-idle-border-color: 'rgba(255,255,255,0.18)'
mdc-text-field-outlined-hover-border-color: '#a78bfa'
mdc-filled-text-field-container-color: 'rgba(22, 24, 40, 0.85)'
mdc-filled-text-field-label-text-color: '#9a9cb8'
mdc-filled-text-field-input-text-color: '#e7e9f4'
ha-textfield-background: 'rgba(22, 24, 40, 0.85)'
# ===== Assist voice-command dialog — card-mod CSS override =====
card-mod-theme: glass_exo
card-mod-root: |
ha-voice-command-dialog $ ha-textfield {
--mdc-text-field-fill-color: rgba(22, 24, 40, 0.92) !important;
--mdc-text-field-ink-color: #e7e9f4 !important;
--mdc-text-field-label-ink-color: #9a9cb8 !important;
--mdc-text-field-idle-line-color: rgba(255,255,255,0.18) !important;
}
ha-voice-command-dialog $ ha-textfield $ .mdc-text-field__input {
color: #e7e9f4 !important;
}
# ===== buttons =====
mdc-theme-primary: '#a78bfa'
mdc-theme-secondary: '#f472b6'
mdc-theme-on-primary: '#ffffff'
mdc-theme-on-secondary: '#ffffff'
# ===== code / graph =====
code-editor-background-color: 'rgba(10, 10, 20, 0.85)'
graph-base-color: '#a78bfa'
# ===== tables =====
table-row-background-color: 'transparent'
table-row-alternative-background-color: 'rgba(255, 255, 255, 0.02)'

View File

@@ -0,0 +1,143 @@
samurai:
# ===== font (classical Japanese + serif) =====
primary-font-family: '"Noto Serif JP", "Sawarabi Mincho", "Shippori Mincho", "Cormorant Garamond", Georgia, serif'
paper-font-common-base_-_font-family: '"Noto Serif JP", "Sawarabi Mincho", Georgia, serif'
paper-font-body1_-_font-family: '"Noto Serif JP", "Sawarabi Mincho", Georgia, serif'
paper-font-subhead_-_font-family: '"Noto Serif JP", "Shippori Mincho", Georgia, serif'
paper-font-headline_-_font-family: '"Shippori Mincho", "Noto Serif JP", Georgia, serif'
paper-font-title_-_font-family: '"Shippori Mincho", "Noto Serif JP", Georgia, serif'
ha-card-header-font-family: '"Shippori Mincho", "Noto Serif JP", Georgia, serif'
# ===== background (sumi ink + rising sun) =====
lovelace-background: 'radial-gradient(900px 700px at 85% 15%, rgba(196, 30, 58, 0.22) 0%, transparent 50%), radial-gradient(1100px 800px at 15% 85%, rgba(212, 175, 55, 0.10) 0%, transparent 55%), linear-gradient(170deg, #0d0d0d 0%, #0a0508 50%, #050000 100%)'
primary-background-color: '#0a0508'
secondary-background-color: '#13090c'
app-header-background-color: 'rgba(10, 5, 8, 0.88)'
app-header-text-color: '#d4af37'
sidebar-background-color: 'rgba(10, 5, 8, 0.94)'
sidebar-text-color: '#c8bfa8'
sidebar-selected-text-color: '#d4af37'
sidebar-selected-background-color: 'rgba(196, 30, 58, 0.18)'
sidebar-icon-color: '#8a8070'
sidebar-selected-icon-color: '#c41e3a'
# ===== card (rice paper on charcoal) =====
ha-card-background: 'rgba(20, 13, 14, 0.94)'
card-background-color: 'rgba(20, 13, 14, 0.94)'
ha-card-border-radius: '2px'
ha-card-border-width: '1px'
ha-card-border-color: 'rgba(212, 175, 55, 0.20)'
ha-card-box-shadow: '0 4px 20px rgba(0, 0, 0, 0.60), inset 0 1px 0 rgba(212, 175, 55, 0.08)'
ha-card-header-color: '#d4af37'
# ===== more-info dialog / popup (opaque, readable) =====
mdc-theme-surface: '#140d0e'
mdc-dialog-scrim-color: 'rgba(0, 0, 0, 0.82)'
dialog-backdrop-filter: 'blur(10px)'
ha-dialog-surface-background: '#140d0e'
ha-dialog-border-radius: '2px'
# ===== text =====
primary-text-color: '#ebe3d0'
secondary-text-color: '#9a8f7a'
text-primary-color: '#ffffff'
disabled-text-color: '#4a4238'
# ===== accents =====
primary-color: '#c41e3a'
accent-color: '#d4af37'
light-primary-color: '#e34f65'
dark-primary-color: '#8b1528'
label-badge-background-color: 'rgba(20, 13, 14, 0.92)'
label-badge-text-color: '#d4af37'
label-badge-red: '#c41e3a'
label-badge-green: '#728a5c'
label-badge-blue: '#4a6a7c'
label-badge-yellow: '#d4af37'
label-badge-grey: '#8a8070'
# ===== state colors =====
state-icon-color: '#d4af37'
state-icon-active-color: '#c41e3a'
state-icon-unavailable-color: '#4a4238'
paper-item-icon-color: '#9a8f7a'
paper-item-icon-active-color: '#c41e3a'
# ===== domain states =====
state-binary-sensor-active-color: '#728a5c'
state-light-active-color: '#d4af37'
state-switch-active-color: '#c41e3a'
state-fan-active-color: '#4a6a7c'
state-media-player-active-color: '#c41e3a'
state-person-home-color: '#728a5c'
state-person-not_home-color: '#8a8070'
# ===== toggles =====
switch-checked-color: '#c41e3a'
switch-checked-button-color: '#e34f65'
switch-checked-track-color: 'rgba(196, 30, 58, 0.45)'
switch-unchecked-button-color: '#4a4238'
switch-unchecked-track-color: 'rgba(74, 66, 56, 0.45)'
# ===== sliders =====
paper-slider-knob-color: '#c41e3a'
paper-slider-knob-start-color: '#c41e3a'
paper-slider-pin-color: '#d4af37'
paper-slider-active-color: '#d4af37'
paper-slider-container-color: 'rgba(196, 30, 58, 0.30)'
paper-slider-secondary-color: '#8b1528'
# ===== dividers / outlines =====
divider-color: 'rgba(212, 175, 55, 0.15)'
outline-color: 'rgba(212, 175, 55, 0.18)'
# ===== input elements =====
input-background-color: 'rgba(20, 13, 14, 0.85)'
input-fill-color: 'rgba(20, 13, 14, 0.85)'
input-ink-color: '#ebe3d0'
input-label-ink-color: '#9a8f7a'
input-idle-line-color: 'rgba(212, 175, 55, 0.22)'
input-hover-line-color: '#d4af37'
input-focused-line-color: '#c41e3a'
# ===== MDC select + text-field (dropdowns inside cards/dialogs) =====
mdc-select-fill-color: 'rgba(20, 13, 14, 0.92)'
mdc-select-ink-color: '#ebe3d0'
mdc-select-label-ink-color: '#9a8f7a'
mdc-select-dropdown-icon-color: '#d4af37'
mdc-select-idle-line-color: 'rgba(212, 175, 55, 0.30)'
mdc-select-hover-line-color: '#d4af37'
mdc-select-focused-label-color: '#c41e3a'
mdc-text-field-fill-color: 'rgba(20, 13, 14, 0.92)'
mdc-text-field-ink-color: '#ebe3d0'
mdc-text-field-label-ink-color: '#9a8f7a'
mdc-text-field-idle-line-color: 'rgba(212, 175, 55, 0.30)'
mdc-text-field-hover-line-color: '#d4af37'
mdc-text-field-focused-label-color: '#c41e3a'
mdc-text-field-disabled-fill-color: 'rgba(20, 13, 14, 0.60)'
mdc-text-field-disabled-ink-color: '#9a8f7a'
mdc-filled-text-field-container-color: 'rgba(20, 13, 14, 0.94)'
mdc-filled-text-field-label-text-color: '#9a8f7a'
mdc-filled-text-field-input-text-color: '#ebe3d0'
ha-textfield-background: 'rgba(20, 13, 14, 0.94)'
card-mod-theme: samurai
card-mod-root: |
ha-voice-command-dialog $ ha-textfield {
--mdc-text-field-fill-color: rgba(20, 13, 14, 0.96) !important;
--mdc-text-field-ink-color: #ebe3d0 !important;
--mdc-text-field-label-ink-color: #9a8f7a !important;
}
ha-voice-command-dialog $ ha-textfield $ .mdc-text-field__input {
color: #ebe3d0 !important;
}
# ===== buttons =====
mdc-theme-primary: '#c41e3a'
mdc-theme-secondary: '#d4af37'
mdc-theme-on-primary: '#ffffff'
mdc-theme-on-secondary: '#0a0508'
# ===== tables =====
table-row-background-color: 'transparent'
table-row-alternative-background-color: 'rgba(212, 175, 55, 0.04)'

View File

@@ -0,0 +1,143 @@
steampunk:
# ===== font (classical, brass-engraved feel) =====
primary-font-family: '"Cormorant Garamond", "Playfair Display", "Crimson Text", Georgia, "Times New Roman", serif'
paper-font-common-base_-_font-family: '"Cormorant Garamond", "Playfair Display", Georgia, serif'
paper-font-body1_-_font-family: '"Cormorant Garamond", "Crimson Text", Georgia, serif'
paper-font-subhead_-_font-family: '"Playfair Display", "Cormorant Garamond", Georgia, serif'
paper-font-headline_-_font-family: '"Playfair Display", "Cormorant Garamond", Georgia, serif'
paper-font-title_-_font-family: '"Playfair Display", "Cormorant Garamond", Georgia, serif'
ha-card-header-font-family: '"Playfair Display", "Cormorant Garamond", Georgia, serif'
# ===== background (aged leather, gaslight) =====
lovelace-background: 'radial-gradient(1200px 800px at 20% 20%, rgba(201, 166, 107, 0.18) 0%, transparent 55%), radial-gradient(1000px 700px at 80% 80%, rgba(184, 115, 51, 0.15) 0%, transparent 55%), linear-gradient(135deg, #2a1810 0%, #1a0e08 50%, #0e0704 100%)'
primary-background-color: '#1a0e08'
secondary-background-color: '#241610'
app-header-background-color: 'rgba(26, 14, 8, 0.85)'
app-header-text-color: '#d9b381'
sidebar-background-color: 'rgba(26, 14, 8, 0.92)'
sidebar-text-color: '#c7a870'
sidebar-selected-text-color: '#e8c98c'
sidebar-selected-background-color: 'rgba(201, 166, 107, 0.15)'
sidebar-icon-color: '#8a6e46'
sidebar-selected-icon-color: '#d9895f'
# ===== card (aged parchment panel) =====
ha-card-background: 'rgba(38, 24, 16, 0.93)'
card-background-color: 'rgba(38, 24, 16, 0.93)'
ha-card-border-radius: '6px'
ha-card-border-width: '1px'
ha-card-border-color: 'rgba(201, 166, 107, 0.25)'
ha-card-box-shadow: '0 6px 24px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(201, 166, 107, 0.12)'
ha-card-header-color: '#e8c98c'
# ===== more-info dialog / popup (opaque, readable) =====
mdc-theme-surface: '#261810'
mdc-dialog-scrim-color: 'rgba(0, 0, 0, 0.80)'
dialog-backdrop-filter: 'blur(10px)'
ha-dialog-surface-background: '#261810'
ha-dialog-border-radius: '8px'
# ===== text =====
primary-text-color: '#f0dcaf'
secondary-text-color: '#b89a66'
text-primary-color: '#2a1810'
disabled-text-color: '#6a5538'
# ===== accents =====
primary-color: '#c9a66b'
accent-color: '#d9895f'
light-primary-color: '#e8c98c'
dark-primary-color: '#8a6e46'
label-badge-background-color: 'rgba(38, 24, 16, 0.90)'
label-badge-text-color: '#e8c98c'
label-badge-red: '#c44a2a'
label-badge-green: '#6b8f47'
label-badge-blue: '#4a7688'
label-badge-yellow: '#d9b54f'
label-badge-grey: '#8a6e46'
# ===== state colors =====
state-icon-color: '#c9a66b'
state-icon-active-color: '#d9895f'
state-icon-unavailable-color: '#6a5538'
paper-item-icon-color: '#b89a66'
paper-item-icon-active-color: '#d9895f'
# ===== domain states =====
state-binary-sensor-active-color: '#8fa054'
state-light-active-color: '#e8b94a'
state-switch-active-color: '#c9a66b'
state-fan-active-color: '#c9a66b'
state-media-player-active-color: '#d9895f'
state-person-home-color: '#8fa054'
state-person-not_home-color: '#8a6e46'
# ===== toggles =====
switch-checked-color: '#d9895f'
switch-checked-button-color: '#e8c98c'
switch-checked-track-color: 'rgba(217, 137, 95, 0.45)'
switch-unchecked-button-color: '#6a5538'
switch-unchecked-track-color: 'rgba(106, 85, 56, 0.45)'
# ===== sliders =====
paper-slider-knob-color: '#d9895f'
paper-slider-knob-start-color: '#d9895f'
paper-slider-pin-color: '#c9a66b'
paper-slider-active-color: '#c9a66b'
paper-slider-container-color: 'rgba(201, 166, 107, 0.30)'
paper-slider-secondary-color: '#8a6e46'
# ===== dividers / outlines =====
divider-color: 'rgba(201, 166, 107, 0.20)'
outline-color: 'rgba(201, 166, 107, 0.22)'
# ===== input elements =====
input-background-color: 'rgba(38, 24, 16, 0.80)'
input-fill-color: 'rgba(38, 24, 16, 0.80)'
input-ink-color: '#f0dcaf'
input-label-ink-color: '#b89a66'
input-idle-line-color: 'rgba(201, 166, 107, 0.30)'
input-hover-line-color: '#c9a66b'
input-focused-line-color: '#d9895f'
# ===== MDC select + text-field (dropdowns inside cards/dialogs) =====
mdc-select-fill-color: 'rgba(38, 24, 16, 0.90)'
mdc-select-ink-color: '#f0dcaf'
mdc-select-label-ink-color: '#b89a66'
mdc-select-dropdown-icon-color: '#d9895f'
mdc-select-idle-line-color: 'rgba(201, 166, 107, 0.35)'
mdc-select-hover-line-color: '#c9a66b'
mdc-select-focused-label-color: '#d9895f'
mdc-text-field-fill-color: 'rgba(38, 24, 16, 0.90)'
mdc-text-field-ink-color: '#f0dcaf'
mdc-text-field-label-ink-color: '#b89a66'
mdc-text-field-idle-line-color: 'rgba(201, 166, 107, 0.35)'
mdc-text-field-hover-line-color: '#c9a66b'
mdc-text-field-focused-label-color: '#d9895f'
mdc-text-field-disabled-fill-color: 'rgba(38, 24, 16, 0.60)'
mdc-text-field-disabled-ink-color: '#b89a66'
mdc-filled-text-field-container-color: 'rgba(38, 24, 16, 0.92)'
mdc-filled-text-field-label-text-color: '#b89a66'
mdc-filled-text-field-input-text-color: '#f0dcaf'
ha-textfield-background: 'rgba(38, 24, 16, 0.92)'
card-mod-theme: steampunk
card-mod-root: |
ha-voice-command-dialog $ ha-textfield {
--mdc-text-field-fill-color: rgba(38, 24, 16, 0.95) !important;
--mdc-text-field-ink-color: #f0dcaf !important;
--mdc-text-field-label-ink-color: #b89a66 !important;
}
ha-voice-command-dialog $ ha-textfield $ .mdc-text-field__input {
color: #f0dcaf !important;
}
# ===== buttons =====
mdc-theme-primary: '#c9a66b'
mdc-theme-secondary: '#d9895f'
mdc-theme-on-primary: '#1a0e08'
mdc-theme-on-secondary: '#1a0e08'
# ===== tables =====
table-row-background-color: 'transparent'
table-row-alternative-background-color: 'rgba(201, 166, 107, 0.05)'

View File

@@ -0,0 +1,201 @@
// Assist textfield readability — v5.
//
// The new ha-input wraps Web Awesome (wa-input). Web Awesome is a separate
// component system that renders LIGHT by default unless told to be dark via:
// 1) adding the "wa-dark" class on the root / document element, or
// 2) setting its own --wa-* CSS color tokens at :root.
//
// HA's dark theme flag doesn't propagate to it, so we do both: force wa-dark
// class on the html element AND override the wa color tokens to match the
// active HA theme. Plus inline-style belt-and-suspenders on the actual input.
(function () {
if (window.__assistFixLoaded) return;
window.__assistFixLoaded = true;
function v(name, fallback) {
const x = getComputedStyle(document.documentElement)
.getPropertyValue(name).trim();
return x || fallback;
}
function applyGlobalTokens() {
const fg = v('--primary-text-color', '#e7e9f4');
const bg = v('--card-background-color', 'rgba(22, 24, 40, 0.92)');
const bgElev = v('--secondary-background-color', 'rgba(30, 32, 50, 0.95)');
const muted = v('--secondary-text-color', '#9a9cb8');
const border = v('--divider-color', 'rgba(255,255,255,0.20)');
const accent = v('--primary-color', '#a78bfa');
// 1. Force Web Awesome dark mode class on the html element
document.documentElement.classList.add('wa-dark');
// 2. Inject/refresh a <style> with wa-* color tokens mapped to HA theme
let el = document.getElementById('assist-fix-wa-tokens');
if (!el) {
el = document.createElement('style');
el.id = 'assist-fix-wa-tokens';
document.head.appendChild(el);
}
el.textContent = `
:root, html.wa-dark, .wa-dark {
/* Web Awesome text tokens */
--wa-color-text: ${fg};
--wa-color-text-link: ${accent};
--wa-color-text-quiet: ${muted};
--wa-color-text-normal: ${fg};
--wa-color-on-quiet: ${fg};
--wa-color-on-normal: ${fg};
/* Surfaces */
--wa-color-surface-default: ${bg};
--wa-color-surface-raised: ${bgElev};
--wa-color-surface-lowered: ${bg};
--wa-color-surface-border: ${border};
/* Fills (inputs, buttons) */
--wa-color-fill-quiet: ${bg};
--wa-color-fill-normal: ${bg};
--wa-color-fill-loud: ${accent};
/* Borders */
--wa-color-border-quiet: ${border};
--wa-color-border-normal: ${border};
--wa-color-border-loud: ${accent};
/* Brand (accent) */
--wa-color-brand-on-quiet: ${fg};
--wa-color-brand-on-normal: ${fg};
--wa-color-brand-fill-quiet: ${bg};
--wa-color-brand-fill-normal: ${accent};
--wa-color-brand-fill-loud: ${accent};
--wa-color-brand-border-quiet: ${border};
--wa-color-brand-border-normal: ${accent};
/* Neutral scale some components read */
--wa-color-neutral-fill-quiet: ${bg};
--wa-color-neutral-fill-normal: ${bg};
--wa-color-neutral-on-quiet: ${fg};
--wa-color-neutral-on-normal: ${fg};
--wa-color-neutral-border-quiet: ${border};
--wa-color-neutral-border-normal: ${border};
/* Shoelace-inspired fallbacks (some versions) */
--sl-color-neutral-0: ${bg};
--sl-color-neutral-50: ${bgElev};
--sl-color-neutral-100: ${bg};
--sl-color-neutral-200: ${border};
--sl-color-neutral-500: ${muted};
--sl-color-neutral-700: ${fg};
--sl-color-neutral-900: ${fg};
--sl-color-neutral-1000: ${fg};
--sl-input-background-color: ${bg};
--sl-input-color: ${fg};
--sl-input-border-color: ${border};
--sl-input-label-color: ${muted};
}
`;
}
function patchHaInput(haInput) {
if (!haInput || haInput.__assistFixed) return;
const sr = haInput.shadowRoot;
if (!sr) return;
haInput.__assistFixed = true;
const fg = v('--primary-text-color', '#e7e9f4');
const bg = v('--card-background-color', 'rgba(22, 24, 40, 0.92)');
const muted = v('--secondary-text-color', '#9a9cb8');
const border = v('--divider-color', 'rgba(255,255,255,0.20)');
// CSS via exported parts of wa-input
const outer = document.createElement('style');
outer.textContent = `
:host, wa-input { color: ${fg} !important; }
wa-input::part(base) {
background-color: ${bg} !important;
border-color: ${border} !important;
color: ${fg} !important;
}
wa-input::part(input) {
color: ${fg} !important;
caret-color: ${fg} !important;
background-color: transparent !important;
}
wa-input::part(label),
wa-input::part(form-control-label) {
color: ${muted} !important;
}
wa-input::part(hint) { color: ${muted} !important; }
`;
sr.appendChild(outer);
// Inline styles on the wa-input's internals (deepest possible)
const wa = sr.querySelector('wa-input');
if (wa && wa.shadowRoot) {
const innerStyle = document.createElement('style');
innerStyle.textContent = `
:host { color: ${fg} !important; }
.text-field, div[part="base"] {
background-color: ${bg} !important;
border-color: ${border} !important;
}
input, input.control {
color: ${fg} !important;
caret-color: ${fg} !important;
background-color: transparent !important;
}
input::placeholder { color: ${muted} !important; opacity: 0.7; }
label, .label { color: ${muted} !important; }
`;
wa.shadowRoot.appendChild(innerStyle);
const input = wa.shadowRoot.querySelector('input');
if (input) {
input.style.setProperty('color', fg, 'important');
input.style.setProperty('caret-color', fg, 'important');
input.style.setProperty('background-color', 'transparent', 'important');
}
const base = wa.shadowRoot.querySelector('[part="base"]');
if (base) {
base.style.setProperty('background-color', bg, 'important');
base.style.setProperty('border-color', border, 'important');
base.style.setProperty('color', fg, 'important');
}
const label = wa.shadowRoot.querySelector('.label, [part~="label"]');
if (label) label.style.setProperty('color', muted, 'important');
}
}
function walk(root, depth = 0) {
if (!root || depth > 12) return 0;
let n = 0;
try {
root.querySelectorAll('ha-input').forEach(el => { patchHaInput(el); n++; });
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) n += walk(el.shadowRoot, depth + 1);
});
} catch (e) {}
return n;
}
applyGlobalTokens();
let n = walk(document);
console.log('[assist-fix] v5 initial ha-input styled:', n);
new MutationObserver(() => { applyGlobalTokens(); walk(document); })
.observe(document.body || document.documentElement,
{ childList: true, subtree: true });
window.addEventListener('show-dialog', () => {
setTimeout(() => {
const nn = walk(document);
console.log('[assist-fix] v5 after show-dialog ha-input styled:', nn);
}, 150);
});
// Re-run tokens whenever theme might change
window.addEventListener('theme-changed', applyGlobalTokens);
let passes = 0;
const iv = setInterval(() => {
const nn = walk(document);
if (nn > n) { console.log('[assist-fix] v5 now styled:', nn); n = nn; }
if (++passes > 240) clearInterval(iv);
}, 500);
console.log('[assist-fix] v5 loaded — wa-dark + wa-* tokens + inline');
})();

View File

@@ -0,0 +1,18 @@
(function() {
if (document.getElementById('vish-themed-fonts')) return;
var link = document.createElement('link');
link.id = 'vish-themed-fonts';
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?' +
'family=Exo+2:wght@300;400;500;600;700&' +
'family=Rajdhani:wght@400;500;600;700&' +
'family=Orbitron:wght@500;600;700;900&' +
'family=Share+Tech+Mono&' +
'family=Cormorant+Garamond:wght@400;500;600;700&' +
'family=Playfair+Display:wght@500;600;700&' +
'family=Crimson+Text:wght@400;600&' +
'family=Noto+Serif+JP:wght@400;500;700&' +
'family=Shippori+Mincho:wght@500;600;700&' +
'family=Sawarabi+Mincho&display=swap';
document.head.appendChild(link);
})();

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Invidious DB initialisation script
# Runs once on first container start (docker-entrypoint-initdb.d).
#
# Adds a pg_hba.conf rule allowing connections from any Docker subnet
# using trust auth. Without this, PostgreSQL rejects the invidious
# container when the Docker network is assigned a different subnet after
# a recreate (the default pg_hba.conf only covers localhost).
set -e
# Allow connections from any host on the Docker bridge network
echo "host all all 0.0.0.0/0 trust" >> /var/lib/postgresql/data/pg_hba.conf

View File

@@ -0,0 +1,115 @@
version: "3"
configs:
materialious_nginx:
content: |
events { worker_connections 1024; }
http {
default_type application/octet-stream;
include /etc/nginx/mime.types;
server {
listen 80;
# The video player passes dashUrl as a relative path that resolves
# to this origin — proxy Invidious API/media paths to local service.
# (in.vish.gg resolves to the external IP which is unreachable via
# hairpin NAT from inside Docker; invidious:3000 is on same network)
location ~ ^/(api|companion|vi|ggpht|videoplayback|sb|s_p|ytc|storyboards) {
proxy_pass http://invidious:3000;
proxy_set_header Host $$host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
}
location / {
root /usr/share/nginx/html;
try_files $$uri /index.html;
}
}
}
services:
invidious:
image: quay.io/invidious/invidious:latest
platform: linux/amd64
restart: unless-stopped
ports:
- "3000:3000"
environment:
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: kemal
password: "REDACTED_PASSWORD"
host: invidious-db
port: 5432
check_tables: true
invidious_companion:
- private_url: "http://companion:8282/companion"
invidious_companion_key: "pha6nuser7ecei1E"
hmac_key: "Kai5eexiewohchei"
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
interval: 30s
timeout: 5s
retries: 2
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
- invidious-db
- companion
companion:
image: quay.io/invidious/invidious-companion:latest
platform: linux/amd64
environment:
- SERVER_SECRET_KEY=pha6nuser7ecei1E
restart: unless-stopped
cap_drop:
- ALL
read_only: true
volumes:
- companioncache:/var/tmp/youtubei.js:rw
security_opt:
- no-new-privileges:true
logging:
options:
max-size: "1G"
max-file: "4"
invidious-db:
image: postgres:14
restart: unless-stopped
environment:
POSTGRES_DB: invidious
POSTGRES_USER: kemal
POSTGRES_PASSWORD: "REDACTED_PASSWORD" # pragma: allowlist secret
volumes:
- postgresdata:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
materialious:
image: wardpearce/materialious:latest
container_name: materialious
restart: unless-stopped
environment:
VITE_DEFAULT_INVIDIOUS_INSTANCE: "https://in.vish.gg"
configs:
- source: materialious_nginx
target: /etc/nginx/nginx.conf
ports:
- "3001:80"
logging:
options:
max-size: "1G"
max-file: "4"
volumes:
postgresdata:
companioncache:

View File

@@ -0,0 +1,4 @@
vish@vish-concord-nuc:~/invidious/invidious$ pwgen 16 1 # for Invidious (HMAC_KEY)
Kai5eexiewohchei
vish@vish-concord-nuc:~/invidious/invidious$ pwgen 16 1 # for Invidious companion (invidious_companion_key)
pha6nuser7ecei1E

View File

@@ -0,0 +1,65 @@
version: "3.8" # Upgrade to a newer version for better features and support
services:
invidious:
image: quay.io/invidious/invidious:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: kemal
password: "REDACTED_PASSWORD"
host: invidious-db
port: 5432
check_tables: true
signature_server: inv_sig_helper:12999
visitor_data: ""
po_token: "REDACTED_TOKEN"=="
hmac_key: "9Uncxo4Ws54s7dr0i3t8"
healthcheck:
test: ["CMD", "wget", "-nv", "--tries=1", "--spider", "http://127.0.0.1:3000/api/v1/trending"]
interval: 30s
timeout: 5s
retries: 2
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
- invidious-db
inv_sig_helper:
image: quay.io/invidious/inv-sig-helper:latest
init: true
command: ["--tcp", "0.0.0.0:12999"]
environment:
- RUST_LOG=info
restart: unless-stopped
cap_drop:
- ALL
read_only: true
security_opt:
- no-new-privileges:true
invidious-db:
image: docker.io/library/postgres:14
restart: unless-stopped
volumes:
- postgresdata:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment:
POSTGRES_DB: invidious
POSTGRES_USER: kemal
POSTGRES_PASSWORD: "REDACTED_PASSWORD"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 30s
timeout: 5s
retries: 3
volumes:
postgresdata:

View File

@@ -0,0 +1,2 @@
docker all in one
docker-compose down --volumes --remove-orphans && docker-compose pull && docker-compose up -d

View File

@@ -0,0 +1,28 @@
# Redirect all HTTP traffic to HTTPS
server {
listen 80;
server_name client.spotify.vish.gg;
return 301 https://$host$request_uri;
}
# HTTPS configuration for the subdomain
server {
listen 443 ssl;
server_name client.spotify.vish.gg;
# SSL Certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/client.spotify.vish.gg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/client.spotify.vish.gg/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# Proxy to Docker container
location / {
proxy_pass http://127.0.0.1:4000; # Maps to your Docker container
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,63 @@
server {
if ($host = in.vish.gg) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name in.vish.gg;
# Redirect all HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name in.vish.gg;
# SSL Certificates (Certbot paths)
ssl_certificate /etc/letsencrypt/live/in.vish.gg/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/in.vish.gg/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# --- Reverse Proxy to Invidious ---
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Required headers for reverse proxying
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket and streaming stability
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Disable buffering for video streams
proxy_buffering off;
proxy_request_buffering off;
# Avoid premature timeouts during long playback
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
# Cache static assets (images, css, js) for better performance
location ~* \.(?:jpg|jpeg|png|gif|ico|css|js|webp)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
proxy_pass http://127.0.0.1:3000;
}
# Security headers (optional but sensible)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy same-origin;
}

View File

@@ -0,0 +1,28 @@
# Redirect HTTP to HTTPS
server {
listen 80;
server_name spotify.vish.gg;
return 301 https://$host$request_uri;
}
# HTTPS server block
server {
listen 443 ssl;
server_name spotify.vish.gg;
# SSL Certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/spotify.vish.gg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/spotify.vish.gg/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Proxy requests to backend API
location / {
proxy_pass http://127.0.0.1:15000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,74 @@
# Redirect HTTP to HTTPS
server {
listen 80;
server_name vp.vish.gg api.vp.vish.gg proxy.vp.vish.gg;
return 301 https://$host$request_uri;
}
# HTTPS Reverse Proxy for Piped
server {
listen 443 ssl http2;
server_name vp.vish.gg;
# SSL Certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/vp.vish.gg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vp.vish.gg/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Proxy requests to Piped Frontend (use Docker service name, NOT 127.0.0.1)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTPS Reverse Proxy for Piped API
server {
listen 443 ssl http2;
server_name api.vp.vish.gg;
# SSL Certificates
ssl_certificate /etc/letsencrypt/live/vp.vish.gg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vp.vish.gg/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Proxy requests to Piped API backend
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTPS Reverse Proxy for Piped Proxy (for video streaming)
server {
listen 443 ssl http2;
server_name proxy.vp.vish.gg;
# SSL Certificates
ssl_certificate /etc/letsencrypt/live/vp.vish.gg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vp.vish.gg/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Proxy video playback requests through ytproxy
location ~ (/videoplayback|/api/v4/|/api/manifest/) {
include snippets/ytproxy.conf;
add_header Cache-Control private always;
proxy_hide_header Access-Control-Allow-Origin;
}
location / {
include snippets/ytproxy.conf;
add_header Cache-Control "public, max-age=604800";
proxy_hide_header Access-Control-Allow-Origin;
}
}

View File

@@ -0,0 +1,24 @@
# Node Exporter - Prometheus metrics exporter for hardware/OS metrics
# Exposes metrics on port 9101 (changed from 9100 due to host conflict)
# Used by: Grafana/Prometheus monitoring stack
# Note: Using bridge network with port mapping instead of host network
# to avoid conflict with host-installed node_exporter
version: "3.8"
services:
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node_exporter
ports:
- "9101:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
restart: unless-stopped

View File

@@ -0,0 +1,79 @@
# Piped - YouTube frontend
# Port: 8080
# Privacy-respecting YouTube
services:
piped-frontend:
image: 1337kavin/piped-frontend:latest
restart: unless-stopped
depends_on:
- piped
environment:
BACKEND_HOSTNAME: api.vp.vish.gg
HTTP_MODE: https
container_name: piped-frontend
piped-proxy:
image: 1337kavin/piped-proxy:latest
restart: unless-stopped
environment:
- UDS=1
volumes:
- piped-proxy:/app/socket
container_name: piped-proxy
piped:
image: 1337kavin/piped:latest
restart: unless-stopped
volumes:
- ./config/config.properties:/app/config.properties:ro
depends_on:
- postgres
container_name: piped-backend
bg-helper:
image: 1337kavin/bg-helper-server:latest
restart: unless-stopped
container_name: piped-bg-helper
nginx:
image: nginx:mainline-alpine
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/pipedapi.conf:/etc/nginx/conf.d/pipedapi.conf:ro
- ./config/pipedproxy.conf:/etc/nginx/conf.d/pipedproxy.conf:ro
- ./config/pipedfrontend.conf:/etc/nginx/conf.d/pipedfrontend.conf:ro
- ./config/ytproxy.conf:/etc/nginx/snippets/ytproxy.conf:ro
- piped-proxy:/var/run/ytproxy
container_name: nginx
depends_on:
- piped
- piped-proxy
- piped-frontend
labels:
- "traefik.enable=true"
- "traefik.http.routers.piped.rule=Host(`FRONTEND_HOSTNAME`, `BACKEND_HOSTNAME`, `PROXY_HOSTNAME`)"
- "traefik.http.routers.piped.entrypoints=websecure"
- "traefik.http.services.piped.loadbalancer.server.port=8080"
postgres:
image: pgautoupgrade/pgautoupgrade:16-alpine
restart: unless-stopped
volumes:
- ./data/db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=piped
- POSTGRES_USER=piped
- POSTGRES_PASSWORD="REDACTED_PASSWORD"
container_name: postgres
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/timezone:/etc/timezone:ro
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_RESTARTING=true
container_name: watchtower
command: piped-frontend piped-backend piped-proxy piped-bg-helper varnish nginx postgres watchtower
volumes:
piped-proxy: null

View File

@@ -0,0 +1,28 @@
# Plex Media Server
# Web UI: http://<host-ip>:32400/web
# Uses Intel QuickSync for hardware transcoding (via /dev/dri)
# Media library mounted from NAS at /mnt/nas
services:
plex:
image: linuxserver/plex:latest
container_name: plex
network_mode: host
environment:
- PUID=1000
- PGID=1000
- TZ=America/Los_Angeles
- UMASK=022
- VERSION=docker
# Get claim token from: https://www.plex.tv/claim/
- PLEX_CLAIM=claim-REDACTED_APP_PASSWORD
volumes:
- /home/vish/docker/plex/config:/config
- /mnt/nas/:/data/media
devices:
# Intel QuickSync for hardware transcoding
- /dev/dri:/dev/dri
security_opt:
- no-new-privileges:true
restart: on-failure:10
# custom-cont-init.d/01-wait-for-nas.sh waits up to 120s for /mnt/nas before starting Plex

View File

@@ -0,0 +1,22 @@
# Portainer Edge Agent - concord-nuc
# Connects to Portainer server on Atlantis (100.83.230.112:8000)
# Deploy: docker compose -f portainer_agent.yaml up -d
services:
portainer_edge_agent:
image: portainer/agent:2.33.7
container_name: portainer_edge_agent
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
- /:/host
- portainer_agent_data:/data
environment:
EDGE: "1"
EDGE_ID: "be02f203-f10c-471a-927c-9ca2adac254c"
EDGE_KEY: "aHR0cDovLzEwMC44My4yMzAuMTEyOjEwMDAwfGh0dHA6Ly8xMDAuODMuMjMwLjExMjo4MDAwfGtDWjVkTjJyNXNnQTJvMEF6UDN4R3h6enBpclFqa05Wa0FCQkU0R1IxWFU9fDQ0MzM5OA"
EDGE_INSECURE_POLL: "1"
volumes:
portainer_agent_data:

View File

@@ -0,0 +1,22 @@
# Scrutiny Collector — concord-nuc (Intel NUC)
#
# Ships SMART data to the hub on homelab-vm.
# NUC typically has one internal NVMe + optionally a SATA SSD.
# Adjust device list: run `lsblk` to see actual drives.
#
# Hub: http://100.67.40.126:8090
services:
scrutiny-collector:
image: ghcr.io/analogj/scrutiny:master-collector
container_name: scrutiny-collector
cap_add:
- SYS_RAWIO
- SYS_ADMIN
volumes:
- /run/udev:/run/udev:ro
devices:
- /dev/sda
environment:
COLLECTOR_API_ENDPOINT: "http://100.67.40.126:8090"
restart: unless-stopped

View File

@@ -0,0 +1,19 @@
# Syncthing - File synchronization
# Port: 8384 (web), 22000 (sync)
# Continuous file synchronization between devices
services:
syncthing:
container_name: syncthing
ports:
- 8384:8384
- 22000:22000/tcp
- 22000:22000/udp
- 21027:21027/udp
environment:
- TZ=America/Los_Angeles
volumes:
- /home/vish/docker/syncthing/config:/config
- /home/vish/docker/syncthing/data1:/data1
- /home/vish/docker/syncthing/data2:/data2
restart: unless-stopped
image: ghcr.io/linuxserver/syncthing

View File

@@ -0,0 +1,25 @@
# WireGuard - VPN server
# Port: 51820/udp
# Modern, fast VPN tunnel
services:
wg-easy:
container_name: wg-easy
image: ghcr.io/wg-easy/wg-easy
environment:
- HASH_PASSWORD="REDACTED_PASSWORD"
- WG_HOST=vishconcord.tplinkdns.com
volumes:
- ./config:/etc/wireguard
- /lib/modules:/lib/modules
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1

View File

@@ -0,0 +1,49 @@
# Your Spotify - Listening statistics
# Port: 3000
# Self-hosted Spotify listening history tracker
version: "3.8"
services:
server:
image: yooooomi/your_spotify_server
restart: unless-stopped
ports:
- "15000:8080" # Expose port 15000 for backend service
depends_on:
- mongo
environment:
- API_ENDPOINT=https://spotify.vish.gg # Public URL for backend
- CLIENT_ENDPOINT=https://spotify-client.vish.gg # Public URL for frontend
- SPOTIFY_PUBLIC=d6b3bda999f042099ce79a8b6e9f9e68 # Spotify app client ID
- SPOTIFY_SECRET=72c650e7a25f441baa245b963003a672 # Spotify app client secret
- SPOTIFY_REDIRECT_URI=https://spotify-client.vish.gg/callback # Redirect URI for OAuth
- CORS=https://spotify-client.vish.gg # Allow frontend's origin
networks:
- spotify_network
mongo:
container_name: mongo
image: mongo:4.4.8
restart: unless-stopped
volumes:
- yourspotify_mongo_data:/data/db # Named volume for persistent storage
networks:
- spotify_network
web:
image: yooooomi/your_spotify_client
restart: unless-stopped
ports:
- "4000:3000" # Expose port 4000 for frontend
environment:
- API_ENDPOINT=https://spotify.vish.gg # URL for backend API
networks:
- spotify_network
volumes:
yourspotify_mongo_data:
driver: local
networks:
spotify_network:
driver: bridge

View File

@@ -0,0 +1,234 @@
# Guava - TrueNAS Scale Server
**Hostname**: guava
**IP Address**: 192.168.0.100
**Tailscale IP**: 100.75.252.64
**Domain**: guava.crista.home
**OS**: TrueNAS Scale 25.04.2.6 (Debian 12 Bookworm)
**Kernel**: 6.12.15-production+truenas
---
## Hardware Specifications
| Component | Specification |
|-----------|---------------|
| **CPU** | 12 cores |
| **RAM** | 30 GB |
| **Storage** | ZFS pools (1.5TB+ available) |
| **Docker** | 27.5.0 |
| **Compose** | v2.32.3 |
---
## Storage Layout
### Boot Pool
- `/` - Root filesystem (433GB available)
- ZFS dataset: `boot-pool/ROOT/25.04.2.6`
### Data Pool (`/mnt/data/`)
| Dataset | Size Used | Purpose |
|---------|-----------|---------|
| `data/guava_turquoise` | 3.0TB / 4.5TB | Primary storage (67% used) |
| `data/photos` | 159GB | Photo storage |
| `data/jellyfin` | 145GB | Media library |
| `data/llama` | 59GB | LLM models |
| `data/plane-data` | ~100MB | Plane.so application data |
| `data/iso` | 556MB | ISO images |
| `data/cocalc` | 324MB | Computational notebook |
| `data/website` | 59MB | Web content |
| `data/openproject` | 13MB | OpenProject (postgres) |
| `data/fasten` | 5.7MB | Health records |
| `data/fenrus` | 3.5MB | Dashboard config |
| `data/medical` | 14MB | Medical records |
| `data/truenas-exporters` | - | Prometheus exporters |
### TrueNAS Apps (`/mnt/.ix-apps/`)
- Docker storage: 28GB used
- App configs and mounts for TrueNAS-managed apps
---
## Network Configuration
| Service | Port | Protocol | URL |
|---------|------|----------|-----|
| Portainer | 31015 | HTTPS | https://guava.crista.home:31015 |
| **Plane.so** | 3080 | HTTP | **http://guava.crista.home:3080** |
| Plane.so HTTPS | 3443 | HTTPS | https://guava.crista.home:3443 |
| Jellyfin | 30013 | HTTP | http://guava.crista.home:30013 |
| Jellyfin HTTPS | 30014 | HTTPS | https://guava.crista.home:30014 |
| Gitea | 30008-30009 | HTTP | http://guava.crista.home:30008 |
| WireGuard | 51827 | UDP | - |
| wg-easy UI | 30058 | HTTP | http://guava.crista.home:30058 |
| Fenrus | 45678 | HTTP | http://guava.crista.home:45678 |
| Fasten | 9090 | HTTP | http://guava.crista.home:9090 |
| Node Exporter | 9100 | HTTP | http://guava.crista.home:9100/metrics |
| nginx | 28888 | HTTP | http://guava.crista.home:28888 |
| iperf3 | 5201 | TCP | - |
| SSH | 22 | TCP | - |
| SMB | 445 | TCP | - |
| Pi-hole DNS | 53 | TCP/UDP | - |
---
## Portainer Access
| Setting | Value |
|---------|-------|
| **URL** | `https://guava.crista.home:31015` |
| **API Endpoint** | `https://localhost:31015/api` (from guava) |
| **Endpoint ID** | 3 (local) |
| **API Token** | `ptr_REDACTED_PORTAINER_TOKEN` |
### API Examples
```bash
# List stacks
curl -sk -H 'X-API-Key: "REDACTED_API_KEY" \
'https://localhost:31015/api/stacks'
# List containers
curl -sk -H 'X-API-Key: "REDACTED_API_KEY" \
'https://localhost:31015/api/endpoints/3/docker/containers/json'
# Create stack from compose string
curl -sk -X POST \
-H 'X-API-Key: "REDACTED_API_KEY" \
-H 'Content-Type: application/json' \
'https://localhost:31015/api/stacks/create/standalone/string?endpointId=3' \
-d '{"name": "my-stack", "REDACTED_APP_PASSWORD": "..."}'
```
---
## Deployed Stacks (Portainer)
| ID | Name | Status | Description |
|----|------|--------|-------------|
| 2 | nginx | ✅ Active | Reverse proxy (:28888) |
| 3 | ddns | ✅ Active | Dynamic DNS updater (crista.love) |
| 4 | llama | ⏸️ Inactive | LLM server |
| 5 | fenrus | ✅ Active | Dashboard (:45678) |
| 8 | fasten | ✅ Active | Health records (:9090) |
| 17 | node-exporter | ✅ Active | Prometheus metrics (:9100) |
| 18 | iperf3 | ✅ Active | Network speed testing (:5201) |
| 25 | cocalc | ⏸️ Inactive | Computational notebook |
| **26** | **plane-stack** | ✅ Active | **Project management (:3080)** |
### TrueNAS-Managed Apps (ix-apps)
| App | Container | Port | Description |
|-----|-----------|------|-------------|
| Portainer | ix-portainer-portainer-1 | 31015 | Container management |
| Gitea | ix-gitea-gitea-1 | 30008-30009 | Git server |
| Gitea DB | ix-gitea-postgres-1 | - | PostgreSQL for Gitea |
| Jellyfin | ix-jellyfin-jellyfin-1 | 30013, 30014 | Media server |
| WireGuard | ix-wg-easy-wg-easy-1 | 30058, 51827/udp | VPN server |
| Tailscale | ix-tailscale-tailscale-1 | - | Mesh VPN |
| Pi-hole | (configured) | 53 | DNS server |
---
## SSH Access
### Via Cloudflare Tunnel
```bash
# Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared
chmod +x /tmp/cloudflared
# SSH config
cat >> ~/.ssh/config << 'EOF'
Host guava
HostName ruled-bowl-dos-jews.trycloudflare.com
User vish
IdentityFile ~/.ssh/id_ed25519
ProxyCommand /tmp/cloudflared access ssh --hostname %h
EOF
# Connect
ssh guava
```
### Direct (Local Network)
```bash
ssh vish@192.168.0.100
```
**Note**: Docker commands require `sudo` on guava.
---
## Services Documentation
### Plane.so
See [plane.yaml](plane.yaml) for the full stack configuration.
| Component | Container | Port | Purpose |
|-----------|-----------|------|---------|
| Frontend | plane-web | 3000 | Web UI |
| Admin | plane-admin | 3000 | Admin panel |
| Space | plane-space | 3000 | Public pages |
| API | plane-api | 8000 | Backend API |
| Worker | plane-worker | 8000 | Background jobs |
| Beat | plane-beat | 8000 | Scheduled tasks |
| Live | plane-live | 3000 | Real-time updates |
| Database | plane-db | 5432 | PostgreSQL |
| Cache | plane-redis | 6379 | Valkey/Redis |
| Queue | plane-mq | 5672 | RabbitMQ |
| Storage | plane-minio | 9000 | MinIO S3 |
| Proxy | plane-proxy | 80/443 | Caddy reverse proxy |
**Access URL**: http://guava.crista.home:3080
**Data Location**: `/mnt/data/plane-data/`
---
## Maintenance
### Backup Locations
| Data | Path | Priority |
|------|------|----------|
| Plane DB | `/mnt/data/plane-data/postgres/` | High |
| Plane Files | `/mnt/data/plane-data/minio/` | High |
| Gitea | `/mnt/.ix-apps/app_mounts/gitea/` | High |
| Jellyfin Config | `/mnt/.ix-apps/app_mounts/jellyfin/config/` | Medium |
| Photos | `/mnt/data/photos/` | High |
### Common Commands
```bash
# Check all containers
sudo docker ps -a
# View stack logs
sudo docker compose -f /path/to/stack logs -f
# Restart a stack via Portainer API
curl -sk -X POST \
-H 'X-API-Key: TOKEN' \
'https://localhost:31015/api/stacks/STACK_ID/stop?endpointId=3'
curl -sk -X POST \
-H 'X-API-Key: TOKEN' \
'https://localhost:31015/api/stacks/STACK_ID/start?endpointId=3'
```
---
## Related Documentation
- [Plane.so Service Docs](../../../docs/services/individual/plane.md)
- [TrueNAS Scale Documentation](https://www.truenas.com/docs/scale/)
- [AGENTS.md](../../../AGENTS.md) - Quick reference for all hosts
---
*Last updated: February 4, 2026*
*Verified via SSH - all services confirmed running*

View File

@@ -0,0 +1,23 @@
Guava CIFS/SMB Shares
data /mnt/data/passionfruit
guava_turquoise /mnt/data/guava_turquoise Backup of turquoise
photos /mnt/data/photos
Global Configuration
Nameservers
Nameserver 1:
1.1.1.1
Nameserver 2:
192.168.0.250
Default Route
IPv4:
192.168.0.1
Hostname:guava
Domain: local
HTTP Proxy:---
Service Announcement: NETBIOS-NS, mDNS, WS-DISCOVERY
Additional Domains:---
Hostname Database:---
Outbound Network:Allow All

View File

@@ -0,0 +1,213 @@
# Plane.so - Self-Hosted Project Management
# Deployed via Portainer on TrueNAS Scale (guava)
# Port: 3080 (HTTP), 3443 (HTTPS)
x-db-env: &db-env
PGHOST: plane-db
PGDATABASE: plane
POSTGRES_USER: plane
POSTGRES_PASSWORD: "REDACTED_PASSWORD"
POSTGRES_DB: plane
POSTGRES_PORT: 5432
PGDATA: /var/lib/postgresql/data
x-redis-env: &redis-env
REDIS_HOST: plane-redis
REDIS_PORT: 6379
REDIS_URL: redis://plane-redis:6379/
x-minio-env: &minio-env
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-planeaccess}
MINIO_ROOT_PASSWORD: "REDACTED_PASSWORD"
x-aws-s3-env: &aws-s3-env
AWS_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-planeaccess}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-planesecret123}
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
AWS_S3_BUCKET_NAME: uploads
x-proxy-env: &proxy-env
APP_DOMAIN: ${APP_DOMAIN:-guava.crista.home}
FILE_SIZE_LIMIT: 52428800
LISTEN_HTTP_PORT: 80
LISTEN_HTTPS_PORT: 443
BUCKET_NAME: uploads
SITE_ADDRESS: :80
x-mq-env: &mq-env
RABBITMQ_HOST: plane-mq
RABBITMQ_PORT: 5672
RABBITMQ_DEFAULT_USER: plane
RABBITMQ_DEFAULT_PASS: "REDACTED_PASSWORD"REDACTED_PASSWORD"
RABBITMQ_DEFAULT_VHOST: plane
RABBITMQ_VHOST: plane
x-live-env: &live-env
API_BASE_URL: http://api:8000
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://guava.crista.home:3080}
DEBUG: 0
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
GUNICORN_WORKERS: 2
USE_MINIO: 1
DATABASE_URL: postgresql://plane:${POSTGRES_PASSWORD:"REDACTED_PASSWORD"
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
AMQP_URL: amqp://plane:${RABBITMQ_PASSWORD:"REDACTED_PASSWORD"
API_KEY_RATE_LIMIT: 60/minute
MINIO_ENDPOINT_SSL: 0
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
services:
web:
image: artifacts.plane.so/makeplane/plane-frontend:stable
container_name: plane-web
restart: unless-stopped
depends_on:
- api
- worker
space:
image: artifacts.plane.so/makeplane/plane-space:stable
container_name: plane-space
restart: unless-stopped
depends_on:
- api
- worker
- web
admin:
image: artifacts.plane.so/makeplane/plane-admin:stable
container_name: plane-admin
restart: unless-stopped
depends_on:
- api
- web
live:
image: artifacts.plane.so/makeplane/plane-live:stable
container_name: plane-live
restart: unless-stopped
environment:
<<: [*live-env, *redis-env]
depends_on:
- api
- web
api:
image: artifacts.plane.so/makeplane/plane-backend:stable
container_name: plane-api
command: ./bin/docker-entrypoint-api.sh
restart: unless-stopped
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
plane-db:
condition: service_healthy
plane-redis:
condition: service_started
plane-mq:
condition: service_started
worker:
image: artifacts.plane.so/makeplane/plane-backend:stable
container_name: plane-worker
command: ./bin/docker-entrypoint-worker.sh
restart: unless-stopped
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
beat-worker:
image: artifacts.plane.so/makeplane/plane-backend:stable
container_name: plane-beat
command: ./bin/docker-entrypoint-beat.sh
restart: unless-stopped
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
migrator:
image: artifacts.plane.so/makeplane/plane-backend:stable
container_name: plane-migrator
command: ./bin/docker-entrypoint-migrator.sh
restart: on-failure
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
plane-db:
condition: service_healthy
plane-redis:
condition: service_started
plane-db:
image: postgres:15.7-alpine
container_name: plane-db
command: postgres -c 'max_connections=1000'
restart: unless-stopped
environment:
<<: *db-env
volumes:
- /mnt/data/plane-data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U plane -d plane"]
interval: 10s
timeout: 5s
retries: 5
plane-redis:
image: valkey/valkey:7.2.11-alpine
container_name: plane-redis
restart: unless-stopped
volumes:
- /mnt/data/plane-data/redis:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
container_name: plane-mq
restart: unless-stopped
environment:
<<: *mq-env
volumes:
- /mnt/data/plane-data/rabbitmq:/var/lib/rabbitmq
plane-minio:
image: minio/minio:latest
container_name: plane-minio
command: server /export --console-address ":9090"
restart: unless-stopped
environment:
<<: *minio-env
volumes:
- /mnt/data/plane-data/minio:/export
proxy:
image: artifacts.plane.so/makeplane/plane-proxy:stable
container_name: plane-proxy
restart: unless-stopped
environment:
<<: *proxy-env
ports:
- "3080:80"
- "3443:443"
depends_on:
- web
- api
- space
- admin
- live
networks:
default:
name: plane-network
driver: bridge

View File

@@ -0,0 +1,25 @@
version: '3.8'
services:
cocalc:
image: sagemathinc/cocalc-docker:latest
container_name: cocalc
restart: unless-stopped
ports:
- "8080:443" # expose CoCalc HTTPS on port 8080
# or "443:443" if you want it directly bound to 443
volumes:
# Persistent project and home directories
- /mnt/data/cocalc/projects:/projects
- /mnt/data/cocalc/home:/home/cocalc
# Optional: shared local "library of documents"
- /mnt/data/cocalc/library:/projects/library
environment:
- TZ=America/Los_Angeles
- COCALC_NATS_AUTH=false # disable NATS auth for standalone use
# - COCALC_ADMIN_PASSWORD="REDACTED_PASSWORD" # optional admin password
# - COCALC_NO_IDLE_TIMEOUT=true # optional: stop idle shutdowns

View File

@@ -0,0 +1,18 @@
version: '3.8'
services:
ddns-crista-love:
image: favonia/cloudflare-ddns:latest
container_name: ddns-crista-love
network_mode: host
restart: unless-stopped
user: "3000:3000"
read_only: true
cap_drop:
- all
security_opt:
- no-new-privileges:true
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- DOMAINS=crista.love,cle.crista.love,cocalc.crista.love,mm.crista.love
- PROXIED=true

View File

@@ -0,0 +1,12 @@
version: "3.9"
services:
fasten:
image: ghcr.io/fastenhealth/fasten-onprem:main
container_name: fasten-onprem
ports:
- "9090:8080"
volumes:
- /mnt/data/fasten/db:/opt/fasten/db
- /mnt/data/fasten/cache:/opt/fasten/cache
restart: unless-stopped

View File

@@ -0,0 +1,19 @@
version: "3.9"
services:
fenrus:
image: revenz/fenrus:latest
container_name: fenrus
healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:3000/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
ports:
- "45678:3000"
volumes:
- /mnt/data/fenrus:/app/data:rw
environment:
TZ: America/Los_Angeles
restart: unless-stopped

View File

@@ -0,0 +1,41 @@
version: "3.9"
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
ports:
- "11434:11434"
environment:
- OLLAMA_KEEP_ALIVE=10m
volumes:
- /mnt/data/llama:/root/.ollama
# --- Optional AMD iGPU offload (experimental on SCALE) ---
# devices:
# - /dev/kfd
# - /dev/dri
# group_add:
# - "video"
# - "render"
# environment:
# - OLLAMA_KEEP_ALIVE=10m
# - HSA_ENABLE_SDMA=0
# - HSA_OVERRIDE_GFX_VERSION=11.0.0
openwebui:
image: ghcr.io/open-webui/open-webui:latest
container_name: open-webui
restart: unless-stopped
depends_on:
- ollama
ports:
- "3000:8080" # browse to http://<truenas-ip>:3000
environment:
# Either var works on recent builds; keeping both for compatibility
- OLLAMA_API_BASE_URL=http://ollama:11434
- OLLAMA_BASE_URL=http://ollama:11434
# Set to "false" to allow open signup without password
- WEBUI_AUTH=true
volumes:
- /mnt/data/llama/open-webui:/app/backend/data

View File

@@ -0,0 +1,10 @@
My recommended use on your setup:
Model Use case
Llama3.1:8b Main general-purpose assistant
Mistral:7b Fast, concise replies & RAG
Qwen2.5:3b Lightweight, quick lookups
Qwen2.5-Coder:7b Dedicated coding tasks
Llama3:8b Legacy/benchmark (optional)
qwen2.5:7b-instruct Writing up emails
deepseek-r1 (chonky but accurate)
deepseek-r1:8b (lighter version of r1 , can run on DS1823xs+)

View File

@@ -0,0 +1,18 @@
version: "3.8"
services:
nginx:
image: nginx:latest
container_name: nginx
volumes:
- /mnt/data/website/html:/usr/share/nginx/html:ro
- /mnt/data/website/conf.d:/etc/nginx/conf.d:ro
ports:
- "28888:80" # 👈 Expose port 28888 on the host
networks:
- web-net
restart: unless-stopped
networks:
web-net:
external: true

View File

@@ -0,0 +1,18 @@
version: "3.9"
services:
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
network_mode: "host"
pid: "host"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'

View File

@@ -0,0 +1,43 @@
# Tdarr Node - NUC-QSV (Intel Quick Sync Video hardware transcoding)
# Runs on Proxmox LXC 103 (tdarr-node)
# Connects to Tdarr Server on Synology (atlantis) at 192.168.0.200
#
# NFS Mounts required in LXC:
# /mnt/media -> 192.168.0.200:/volume1/data/media
# /mnt/cache -> 192.168.0.200:/volume3/usenet
#
# Important: Both /temp and /cache must be mounted to the same base path
# as the server's cache to avoid path mismatch errors during file operations.
services:
tdarr-node:
image: ghcr.io/haveagitgat/tdarr_node@sha256:dc23becc667f77d2489b1042REDACTED_GITEA_TOKEN # v2.67.01 - pinned to match server
container_name: tdarr-node
labels:
- com.centurylinklabs.watchtower.enable=false
security_opt:
- apparmor:unconfined
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- nodeName=NUC
- serverIP=192.168.0.200
- serverPort=8266
- inContainer=true
- ffmpegVersion=6
devices:
- /dev/dri:/dev/dri # Intel QSV hardware acceleration
volumes:
- ./configs:/app/configs
- ./logs:/app/logs
- /mnt/media:/media
- /mnt/cache/tdarr_cache:/temp # Server uses both /temp and /cache
- /mnt/cache/tdarr_cache:/cache # Must mount both for node compatibility
restart: unless-stopped
# Auto-update: DISABLED — tdarr nodes must stay version-synced with the server.
# Image is pinned to a specific digest. To update all nodes at once, change the
# digest in all 4 compose files and redeploy simultaneously.
# Remove the hourly cron if it exists: pct exec 103 -- rm -f /etc/cron.d/tdarr-update

View File

View File

@@ -0,0 +1,19 @@
# Ubuntu archive
sudo rsync -avz --delete --ignore-errors --no-perms --no-owner --no-group \
rsync://archive.ubuntu.com/ubuntu \
/volume1/archive/repo/mirror/archive.ubuntu.com/ubuntu
# Ubuntu security
sudo rsync -avz --delete --ignore-errors --no-perms --no-owner --no-group \
rsync://security.ubuntu.com/ubuntu \
/volume1/archive/repo/mirror/security.ubuntu.com/ubuntu
# Debian archive
sudo rsync -avz --delete --ignore-errors --no-perms --no-owner --no-group \
rsync://deb.debian.org/debian \
/volume1/archive/repo/mirror/deb.debian.org/debian
# Debian security
sudo rsync -avz --delete --ignore-errors --no-perms --no-owner --no-group \
rsync://security.debian.org/debian-security \
/volume1/archive/repo/mirror/security.debian.org/debian-security

View File

@@ -0,0 +1,24 @@
# AdGuard Home — Atlantis (backup DNS)
# Port: 53 (DNS), 9080 (web UI)
# Purpose: Backup split-horizon DNS resolver
# Primary: Calypso (192.168.0.250)
# Backup: Atlantis (192.168.0.200) ← this instance
#
# Same filters, rewrites, and upstream DNS as Calypso.
# Router DHCP: primary=192.168.0.250, secondary=192.168.0.200
services:
adguard:
image: adguard/adguardhome:latest
container_name: AdGuard
network_mode: host
mem_limit: 2g
cpu_shares: 768
security_opt:
- no-new-privileges:true
restart: on-failure:5
volumes:
- /volume1/docker/adguard/config:/opt/adguardhome/conf:rw
- /volume1/docker/adguard/data:/opt/adguardhome/work:rw
environment:
TZ: America/Los_Angeles

View File

@@ -0,0 +1,41 @@
# AnythingLLM - Local RAG-powered document assistant
# URL: http://192.168.0.200:3101
# Port: 3101
# LLM: Olares qwen3:32b via OpenAI-compatible API
# Docs: docs/services/individual/anythingllm.md
services:
anythingllm:
image: mintplexlabs/anythingllm:latest
container_name: anythingllm
hostname: anythingllm
security_opt:
- no-new-privileges:true
ports:
- "3101:3001"
volumes:
- /volume2/metadata/docker/anythingllm/storage:/app/server/storage:rw
- /volume1/archive/paperless/backup_2026-03-15/media/documents/archive:/documents/paperless-archive:ro
- /volume1/archive/paperless/backup_2026-03-15/media/documents/originals:/documents/paperless-originals:ro
environment:
STORAGE_DIR: /app/server/storage
SERVER_PORT: 3001
DISABLE_TELEMETRY: "true"
TZ: America/Los_Angeles
# LLM Provider - Olares qwen3:32b via OpenAI-compatible API
LLM_PROVIDER: generic-openai
GENERIC_OPEN_AI_BASE_PATH: http://192.168.0.145:31434/v1
GENERIC_OPEN_AI_MODEL_PREF: qwen3-coder:latest
GENERIC_OPEN_AI_MAX_TOKENS: 8192
GENERIC_OPEN_AI_API_KEY: not-needed # pragma: allowlist secret
GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT: 65536
# Embedding and Vector DB
EMBEDDING_ENGINE: native
VECTOR_DB: lancedb
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/ping"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped

View File

@@ -0,0 +1,498 @@
# Arr Suite - Media automation stack
# Services: Sonarr, Radarr, Prowlarr, Bazarr, Lidarr, Tdarr, LazyLibrarian, Audiobookshelf
# Manages TV shows, movies, music, books, audiobooks downloads and organization
# GitOps Test: Stack successfully deployed and auto-updating
#
# Storage Configuration (2026-02-01):
# - Downloads: /volume3/usenet (Synology SNV5420 NVMe RAID1 - 621 MB/s)
# - Media: /volume1/data (SATA RAID6 - 84TB)
# - Configs: /volume2/metadata/docker2 (Crucial P310 NVMe RAID1)
#
# Volume 3 created for fast download performance using 007revad's Synology_M2_volume script
#
# Theming: Self-hosted theme.park (Dracula theme)
# - TP_DOMAIN uses docker gateway IP to reach host's theme-park container
# - Deploy theme-park stack first: Atlantis/theme-park/theme-park.yaml
version: "3.8"
x-themepark: &themepark
TP_SCHEME: "http"
TP_DOMAIN: "192.168.0.200:8580"
TP_THEME: "dracula"
networks:
media2_net:
driver: bridge
name: media2_net
ipam:
config:
- subnet: 172.24.0.0/24
gateway: 172.24.0.1
services:
wizarr:
image: ghcr.io/wizarrrr/wizarr:latest
container_name: wizarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- DISABLE_BUILTIN_AUTH=true
volumes:
- /volume2/metadata/docker2/wizarr:/data/database
ports:
- "5690:5690"
networks:
media2_net:
ipv4_address: 172.24.0.2
security_opt:
- no-new-privileges:true
restart: unless-stopped
tautulli:
image: lscr.io/linuxserver/tautulli:latest
container_name: tautulli
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:tautulli
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/tautulli:/config
ports:
- "8181:8181"
networks:
media2_net:
ipv4_address: 172.24.0.12
security_opt:
- no-new-privileges:true
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:prowlarr
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/prowlarr:/config
ports:
- "9696:9696"
networks:
media2_net:
ipv4_address: 172.24.0.6
security_opt:
- no-new-privileges:true
restart: unless-stopped
flaresolverr:
image: flaresolverr/flaresolverr:latest
container_name: flaresolverr
environment:
- TZ=America/Los_Angeles
ports:
- "8191:8191"
networks:
media2_net:
ipv4_address: 172.24.0.4
security_opt:
- no-new-privileges:true
restart: unless-stopped
sabnzbd:
image: lscr.io/linuxserver/sabnzbd:latest
container_name: sabnzbd
network_mode: host
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:sabnzbd
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/sabnzbd:/config
- /volume3/usenet/incomplete:/data/incomplete
- /volume3/usenet/complete:/data/complete
security_opt:
- no-new-privileges:true
restart: unless-stopped
jackett:
image: lscr.io/linuxserver/jackett:latest
container_name: jackett
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:jackett
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/jackett:/config
- /volume1/data:/downloads
ports:
- "9117:9117"
networks:
media2_net:
ipv4_address: 172.24.0.11
security_opt:
- no-new-privileges:true
restart: unless-stopped
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:sonarr
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/sonarr:/config
- /volume1/data:/data
- /volume3/usenet:/sab
- /volume2/torrents:/downloads # Deluge download dir — required for torrent import
ports:
- "8989:8989"
networks:
media2_net:
ipv4_address: 172.24.0.7
security_opt:
- no-new-privileges:true
restart: unless-stopped
lidarr:
image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:lidarr
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/lidarr:/config
- /volume1/data:/data
- /volume3/usenet:/sab
# arr-scripts: custom init scripts for Deezer integration via deemix
# Config: /volume2/metadata/docker2/lidarr/extended.conf (contains ARL token, not in git)
# Setup: https://github.com/RandomNinjaAtk/arr-scripts
- /volume2/metadata/docker2/lidarr-scripts/custom-services.d:/custom-services.d
- /volume2/metadata/docker2/lidarr-scripts/custom-cont-init.d:/custom-cont-init.d
ports:
- "8686:8686"
networks:
media2_net:
ipv4_address: 172.24.0.9
security_opt:
- no-new-privileges:true
restart: unless-stopped
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:radarr
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/radarr:/config
- /volume1/data:/data
- /volume3/usenet:/sab
- /volume2/torrents:/downloads # Deluge download dir — required for torrent import
ports:
- "7878:7878"
networks:
media2_net:
ipv4_address: 172.24.0.8
security_opt:
- no-new-privileges:true
restart: unless-stopped
# Readarr retired - replaced with LazyLibrarian + Audiobookshelf
lazylibrarian:
image: lscr.io/linuxserver/lazylibrarian:latest
container_name: lazylibrarian
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:lazylibrarian|ghcr.io/linuxserver/mods:lazylibrarian-calibre
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/lazylibrarian:/config
- /volume1/data:/data
- /volume3/usenet:/sab
- /volume2/torrents:/downloads # Deluge download dir — required for torrent import
- /volume2/metadata/docker2/lazylibrarian-scripts/custom-cont-init.d:/custom-cont-init.d # patch tracker-less torrent handling
ports:
- "5299:5299"
networks:
media2_net:
ipv4_address: 172.24.0.5
security_opt:
- no-new-privileges:true
restart: unless-stopped
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
volumes:
- /volume2/metadata/docker2/audiobookshelf:/config
- /volume1/data/media/audiobooks:/audiobooks
- /volume1/data/media/podcasts:/podcasts
- /volume1/data/media/ebooks:/ebooks
ports:
- "13378:80"
networks:
media2_net:
ipv4_address: 172.24.0.16
security_opt:
- no-new-privileges:true
restart: unless-stopped
# Bazarr - subtitle management for Sonarr and Radarr
# Web UI: http://192.168.0.200:6767
# Language profile: English (profile ID 1), no mustContain filter
# Providers: REDACTED_APP_PASSWORD (vishinator), podnapisi, yifysubtitles, subf2m, subsource, subdl, animetosho
# NOTE: OpenSubtitles.com may be IP-blocked — submit unblock request at opensubtitles.com/support
# Notifications: Signal API via homelab-vm:8080 → REDACTED_PHONE_NUMBER
# API keys stored in: /volume2/metadata/docker2/bazarr/config/config.yaml (not in repo)
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:bazarr
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/bazarr:/config
- /volume1/data:/data
- /volume3/usenet:/sab
ports:
- "6767:6767"
networks:
media2_net:
ipv4_address: 172.24.0.10
security_opt:
- no-new-privileges:true
restart: unless-stopped
whisparr:
image: ghcr.io/hotio/whisparr:nightly
container_name: whisparr
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- TP_HOTIO=true
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/whisparr:/config
- /volume1/data:/data
- /volume3/usenet/complete:/sab/complete
- /volume3/usenet/incomplete:/sab/incomplete
ports:
- "6969:6969"
networks:
media2_net:
ipv4_address: 172.24.0.3
security_opt:
- no-new-privileges:true
restart: unless-stopped
plex:
image: lscr.io/linuxserver/plex:latest
container_name: plex
network_mode: host
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- VERSION=docker
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:plex
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/plex:/config
- /volume1/data/media:/data/media
security_opt:
- no-new-privileges:true
restart: unless-stopped
jellyseerr:
image: fallenbagel/jellyseerr:latest
container_name: jellyseerr
user: "1029:100"
environment:
- TZ=America/Los_Angeles
# Note: Jellyseerr theming requires CSS injection via reverse proxy or browser extension
# theme.park doesn't support DOCKER_MODS for non-linuxserver images
volumes:
- /volume2/metadata/docker2/jellyseerr:/app/config
ports:
- "5055:5055"
networks:
media2_net:
ipv4_address: 172.24.0.14
dns:
- 9.9.9.9
- 1.1.1.1
security_opt:
- no-new-privileges:true
restart: unless-stopped
gluetun:
image: qmcgaw/gluetun:v3.38.0
container_name: gluetun
privileged: true
devices:
- /dev/net/tun:/dev/net/tun
labels:
- com.centurylinklabs.watchtower.enable=false
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
# --- WireGuard ---
- VPN_SERVICE_PROVIDER=custom
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=aAavqcZ6sx3IlgiH5Q8m/6w33mBu4M23JBM8N6cBKEU= # pragma: allowlist secret
- WIREGUARD_ADDRESSES=10.2.0.2/32
- WIREGUARD_DNS=10.2.0.1
- WIREGUARD_PUBLIC_KEY=FrVOQ+Dy0StjfwNtbJygJCkwSJt6ynlGbQwZBZWYfhc=
- WIREGUARD_ALLOWED_IPS=0.0.0.0/0,::/0
- WIREGUARD_ENDPOINT_IP=79.127.185.193
- WIREGUARD_ENDPOINT_PORT=51820
volumes:
- /volume2/metadata/docker2/gluetun:/gluetun
ports:
- "8112:8112" # Deluge WebUI
- "58946:58946" # Torrent TCP
- "58946:58946/udp" # Torrent UDP
networks:
media2_net:
ipv4_address: 172.24.0.20
healthcheck:
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1:9999 2>/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
security_opt:
- no-new-privileges:true
restart: unless-stopped
deluge:
image: lscr.io/linuxserver/deluge:latest
container_name: deluge
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- DOCKER_MODS=ghcr.io/themepark-dev/theme.park:deluge
- TP_SCHEME=http
- TP_DOMAIN=192.168.0.200:8580
- TP_THEME=dracula
volumes:
- /volume2/metadata/docker2/deluge:/config
- /volume2/torrents:/downloads
network_mode: "service:gluetun"
depends_on:
gluetun:
condition: service_healthy
security_opt:
- no-new-privileges:true
restart: unless-stopped
tdarr:
image: ghcr.io/haveagitgat/tdarr@sha256:048ae8ed4de8e9f0de51ad73REDACTED_GITEA_TOKEN # v2.67.01 - pinned to keep all nodes in sync
container_name: tdarr
labels:
- com.centurylinklabs.watchtower.enable=false
environment:
- PUID=1029
- PGID=100
- TZ=America/Los_Angeles
- UMASK=022
- serverIP=0.0.0.0
- serverPort=8266
- webUIPort=8265
- internalNode=true
- inContainer=true
- ffmpegVersion=6
- nodeName=Atlantis
volumes:
- /volume2/metadata/docker2/tdarr/server:/app/server
- /volume2/metadata/docker2/tdarr/configs:/app/configs
- /volume2/metadata/docker2/tdarr/logs:/app/logs
- /volume1/data/media:/media
- /volume3/usenet/tdarr_cache:/temp
- /volume3/usenet/tdarr_cache:/cache # Fix: internal node uses /cache path
ports:
- "8265:8265"
- "8266:8266"
networks:
media2_net:
ipv4_address: 172.24.0.15
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# =============================================================================
# Arr-Suite Installer — Atlantis (192.168.0.200)
# =============================================================================
# One-line install:
# bash <(curl -fsSL https://git.vish.gg/Vish/homelab/raw/branch/main/hosts/synology/atlantis/arr-suite/install.sh)
#
# What this installs:
# Sonarr, Radarr, Lidarr, Bazarr, Prowlarr, Jackett, FlaresolverR
# SABnzbd, Deluge (via gluetun VPN), Tdarr, LazyLibrarian
# Audiobookshelf, Whisparr, Plex, Jellyseerr, Tautulli, Wizarr
#
# Prerequisites:
# - Synology DSM with Container Manager (Docker)
# - /volume1/data, /volume2/metadata/docker2, /volume3/usenet, /volume2/torrents
# - PUID=1029, PGID=100 (DSM user: vish)
# - WireGuard credentials for gluetun (must be set in compose or env)
# =============================================================================
set -euo pipefail
REPO_URL="https://git.vish.gg/Vish/homelab"
COMPOSE_URL="${REPO_URL}/raw/branch/main/hosts/synology/atlantis/arr-suite/docker-compose.yml"
DOCKER="${DOCKER_BIN:-/usr/local/bin/docker}"
STACK_DIR="/volume2/metadata/docker2/arr-suite"
COMPOSE_FILE="${STACK_DIR}/docker-compose.yml"
# Colours
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
# ── Preflight ─────────────────────────────────────────────────────────────────
info "Arr-Suite installer starting"
[[ $(id -u) -eq 0 ]] || error "Run as root (sudo bash install.sh)"
command -v "$DOCKER" &>/dev/null || error "Docker not found at $DOCKER — set DOCKER_BIN env var"
for vol in /volume1/data /volume2/metadata/docker2 /volume3/usenet /volume2/torrents; do
[[ -d "$vol" ]] || warn "Volume $vol does not exist — create it before starting services"
done
# ── Required directories ───────────────────────────────────────────────────────
info "Creating config directories..."
SERVICES=(
sonarr radarr lidarr bazarr prowlarr jackett sabnzbd
deluge gluetun tdarr/server tdarr/configs tdarr/logs
lazylibrarian audiobookshelf whisparr plex jellyseerr
tautulli wizarr
)
for svc in "${SERVICES[@]}"; do
mkdir -p "/volume2/metadata/docker2/${svc}"
done
# Download directories
mkdir -p \
/volume3/usenet/complete \
/volume3/usenet/incomplete \
/volume3/usenet/tdarr_cache \
/volume2/torrents/complete \
/volume2/torrents/incomplete
# Media library
mkdir -p \
/volume1/data/media/tv \
/volume1/data/media/movies \
/volume1/data/media/music \
/volume1/data/media/audiobooks \
/volume1/data/media/podcasts \
/volume1/data/media/ebooks \
/volume1/data/media/misc
# Lidarr arr-scripts directories
mkdir -p \
/volume2/metadata/docker2/lidarr-scripts/custom-cont-init.d \
/volume2/metadata/docker2/lidarr-scripts/custom-services.d
# ── Lidarr arr-scripts bootstrap ──────────────────────────────────────────────
INIT_SCRIPT="/volume2/metadata/docker2/lidarr-scripts/custom-cont-init.d/scripts_init.bash"
if [[ ! -f "$INIT_SCRIPT" ]]; then
info "Downloading arr-scripts init script..."
curl -fsSL "https://raw.githubusercontent.com/RandomNinjaAtk/arr-scripts/main/lidarr/scripts_init.bash" \
-o "$INIT_SCRIPT" || warn "Failed to download arr-scripts init — download manually from RandomNinjaAtk/arr-scripts"
chmod +x "$INIT_SCRIPT"
fi
# ── Download compose file ──────────────────────────────────────────────────────
info "Downloading docker-compose.yml..."
mkdir -p "$STACK_DIR"
curl -fsSL "$COMPOSE_URL" -o "$COMPOSE_FILE" || error "Failed to download compose file from $COMPOSE_URL"
# ── Warn about secrets ────────────────────────────────────────────────────────
warn "==================================================================="
warn "ACTION REQUIRED before starting:"
warn ""
warn "1. Set gluetun WireGuard credentials in:"
warn " $COMPOSE_FILE"
warn " - WIREGUARD_PRIVATE_KEY"
warn " - WIREGUARD_PUBLIC_KEY"
warn " - WIREGUARD_ENDPOINT_IP"
warn ""
warn "2. Set Lidarr Deezer ARL token:"
warn " /volume2/metadata/docker2/lidarr/extended.conf"
warn " arlToken=\"<your-arl-token>\""
warn " Get from: deezer.com -> DevTools -> Cookies -> arl"
warn ""
warn "3. Set Plex claim token (optional, for initial setup):"
warn " https://www.plex.tv/claim"
warn " Add to compose: PLEX_CLAIM=<token>"
warn "==================================================================="
# ── Pull images ───────────────────────────────────────────────────────────────
read -rp "Pull all images now? (y/N): " pull_images
if [[ "${pull_images,,}" == "y" ]]; then
info "Pulling images (this may take a while)..."
"$DOCKER" compose -f "$COMPOSE_FILE" pull
fi
# ── Start stack ───────────────────────────────────────────────────────────────
read -rp "Start all services now? (y/N): " start_services
if [[ "${start_services,,}" == "y" ]]; then
info "Starting arr-suite..."
"$DOCKER" compose -f "$COMPOSE_FILE" up -d
info "Done! Services starting..."
echo ""
echo "Service URLs:"
echo " Sonarr: http://192.168.0.200:8989"
echo " Radarr: http://192.168.0.200:7878"
echo " Lidarr: http://192.168.0.200:8686"
echo " Prowlarr: http://192.168.0.200:9696"
echo " SABnzbd: http://192.168.0.200:8080"
echo " Deluge: http://192.168.0.200:8112 (password: "REDACTED_PASSWORD"
echo " Bazarr: http://192.168.0.200:6767"
echo " Tdarr: http://192.168.0.200:8265"
echo " Whisparr: http://192.168.0.200:6969"
echo " Plex: http://192.168.0.200:32400/web"
echo " Jellyseerr: http://192.168.0.200:5055"
echo " Audiobookshelf:http://192.168.0.200:13378"
echo " LazyLibrarian: http://192.168.0.200:5299"
echo " Tautulli: http://192.168.0.200:8181"
echo " Wizarr: http://192.168.0.200:5690"
echo " Jackett: http://192.168.0.200:9117"
fi
info "Install complete."
info "Docs: https://git.vish.gg/Vish/homelab/src/branch/main/docs/services/individual/"

View File

@@ -0,0 +1,18 @@
services:
jellyseerr:
image: fallenbagel/jellyseerr:latest
container_name: jellyseerr
user: 1029:65536 #YOUR_UID_AND_GID
environment:
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
volumes:
- /volume1/docker2/jellyseerr:/app/config
ports:
- 5055:5055/tcp
network_mode: synobridge
dns: #DNS Servers to help with speed issues some have
- 9.9.9.9
- 1.1.1.1
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,163 @@
# =============================================================================
# PLEX MEDIA SERVER - DISASTER RECOVERY CONFIGURATION
# =============================================================================
#
# SERVICE OVERVIEW:
# - Primary media streaming server for homelab
# - Serves 4K movies, TV shows, music, and photos
# - Hardware transcoding enabled via Intel Quick Sync
# - Critical service for media consumption
#
# DISASTER RECOVERY NOTES:
# - Configuration stored in /volume1/docker2/plex (CRITICAL BACKUP)
# - Media files in /volume1/data/media (128TB+ library)
# - Database contains watch history, metadata, user preferences
# - Hardware transcoding requires Intel GPU access (/dev/dri)
#
# BACKUP PRIORITY: HIGH
# - Config backup: Daily automated backup required
# - Media backup: Secondary NAS sync (Calypso)
# - Database backup: Included in config volume
#
# RECOVERY TIME OBJECTIVE (RTO): 30 minutes
# RECOVERY POINT OBJECTIVE (RPO): 24 hours
#
# DEPENDENCIES:
# - Volume1 must be accessible (current issue: SSD cache failure)
# - Intel GPU drivers for hardware transcoding
# - Network connectivity for remote access
# - Plex Pass subscription for premium features
#
# PORTS USED:
# - 32400/tcp: Main Plex web interface and API
# - 3005/tcp: Plex Home Theater via Plex Companion
# - 8324/tcp: Plex for Roku via Plex Companion
# - 32469/tcp: Plex DLNA Server
# - 1900/udp: Plex DLNA Server
# - 32410/udp, 32412/udp, 32413/udp, 32414/udp: GDM Network discovery
#
# =============================================================================
services:
plex:
# CONTAINER IMAGE:
# - linuxserver/plex: Community-maintained, regularly updated
# - Alternative: plexinc/pms-docker (official but less frequent updates)
# - Version pinning recommended for production: linuxserver/plex:1.32.8
image: linuxserver/plex:latest
# CONTAINER NAME:
# - Fixed name for easy identification and management
# - Used in monitoring, logs, and backup scripts
container_name: plex
# NETWORK CONFIGURATION:
# - host mode: Required for Plex auto-discovery and DLNA
# - Allows Plex to bind to all network interfaces
# - Enables UPnP/DLNA functionality for smart TVs
# - SECURITY NOTE: Exposes all container ports to host
network_mode: host
environment:
# USER/GROUP PERMISSIONS:
# - PUID=1029: User ID for file ownership (Synology 'admin' user)
# - PGID=65536: Group ID for file access (Synology 'administrators' group)
# - CRITICAL: Must match NAS user/group for file access
# - Find correct values: id admin (on Synology)
- PUID=1029 #CHANGE_TO_YOUR_UID
- PGID=65536 #CHANGE_TO_YOUR_GID
# TIMEZONE CONFIGURATION:
# - TZ: Timezone for logs, scheduling, and metadata
# - Must match system timezone for accurate timestamps
# - Format: Area/City (e.g., America/Los_Angeles, Europe/London)
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
# FILE PERMISSIONS:
# - UMASK=022: Default file permissions (755 for dirs, 644 for files)
# - Ensures proper read/write access for media files
# - 022 = owner: rwx, group: r-x, other: r-x
- UMASK=022
# PLEX VERSION MANAGEMENT:
# - VERSION=docker: Use version bundled with Docker image
# - Alternative: VERSION=latest (auto-update, not recommended for production)
# - Alternative: VERSION=1.32.8.7639-fb6452ebf (pin specific version)
- VERSION=docker
# PLEX CLAIM TOKEN:
# - Used for initial server setup and linking to Plex account
# - Get token from: https://plex.tv/claim (valid for 4 minutes)
# - Leave empty after initial setup
# - SECURITY: Remove token after claiming server
- PLEX_CLAIM=
volumes:
# CONFIGURATION VOLUME:
# - /volume1/docker2/plex:/config
# - Contains: Database, metadata, thumbnails, logs, preferences
# - SIZE: ~50-100GB depending on library size
# - BACKUP CRITICAL: Contains all user data and settings
# - RECOVERY: Restore this volume to recover complete Plex setup
- /volume1/docker2/plex:/config
# MEDIA VOLUME:
# - /volume1/data/media:/data/media
# - Contains: Movies, TV shows, music, photos (128TB+ library)
# - READ-ONLY recommended for security (add :ro suffix if desired)
# - STRUCTURE: Organized by type (movies/, tv/, music/, photos/)
# - BACKUP: Synced to Calypso NAS for redundancy
- /volume1/data/media:/data/media
devices:
# HARDWARE TRANSCODING:
# - /dev/dri:/dev/dri: Intel Quick Sync Video access
# - Enables hardware-accelerated transcoding (H.264, H.265, AV1)
# - CRITICAL: Reduces CPU usage by 80-90% during transcoding
# - REQUIREMENT: Intel GPU with Quick Sync support
# - TROUBLESHOOTING: Check 'ls -la /dev/dri' for render devices
- /dev/dri:/dev/dri
security_opt:
# SECURITY HARDENING:
# - no-new-privileges: Prevents privilege escalation attacks
# - Container cannot gain additional privileges during runtime
# - Recommended security practice for all containers
- no-new-privileges:true
# RESTART POLICY:
# - always: Container restarts automatically on failure or system reboot
# - CRITICAL: Ensures Plex is always available for media streaming
# - Alternative: unless-stopped (won't restart if manually stopped)
restart: unless-stopped
# =============================================================================
# DISASTER RECOVERY PROCEDURES:
# =============================================================================
#
# BACKUP VERIFICATION:
# docker exec plex ls -la /config/Library/Application\ Support/Plex\ Media\ Server/
#
# MANUAL BACKUP:
# tar -czf /volume2/backups/plex-config-$(date +%Y%m%d).tar.gz /volume1/docker2/plex/
#
# RESTORE PROCEDURE:
# 1. Stop container: docker-compose down
# 2. Restore config: tar -xzf plex-backup.tar.gz -C /volume1/docker2/
# 3. Fix permissions: chown -R 1029:65536 /volume1/docker2/plex/
# 4. Start container: docker-compose up -d
# 5. Verify: Check http://atlantis.vish.local:32400/web
#
# TROUBLESHOOTING:
# - No hardware transcoding: Check /dev/dri permissions and Intel GPU drivers
# - Database corruption: Restore from backup or rebuild library
# - Permission errors: Verify PUID/PGID match NAS user/group
# - Network issues: Check host networking and firewall rules
#
# MONITORING:
# - Health check: curl -f http://localhost:32400/identity
# - Logs: docker logs plex
# - Transcoding: Plex Dashboard > Settings > Transcoder
# - Performance: Grafana dashboard for CPU/GPU usage
#
# =============================================================================

View File

@@ -0,0 +1,29 @@
services:
linuxserver-prowlarr:
image: linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=1029 #CHANGE_TO_YOUR_UID
- PGID=65536 #CHANGE_TO_YOUR_GID
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
- UMASK=022
volumes:
- /volume1/docker2/prowlarr:/config
ports:
- 9696:9696/tcp
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped
flaresolverr:
image: flaresolverr/flaresolverr:latest
container_name: flaresolverr
environment:
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
ports:
- 8191:8191
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,18 @@
services:
sabnzbd:
image: linuxserver/sabnzbd:latest
container_name: sabnzbd
environment:
- PUID=1029 #CHANGE_TO_YOUR_UID
- PGID=65536 #CHANGE_TO_YOUR_GID
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
- UMASK=022
volumes:
- /volume1/docker2/sabnzbd:/config
- /volume1/data/usenet:/data/usenet
ports:
- 8080:8080/tcp
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,17 @@
services:
tautulli:
image: linuxserver/tautulli:latest
container_name: tautulli
environment:
- PUID=1029 #CHANGE_TO_YOUR_UID
- PGID=65536 #CHANGE_TO_YOUR_GID
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
- UMASK=022
volumes:
- /volume1/docker2/tautulli:/config
ports:
- 8181:8181/tcp
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,18 @@
services:
whisparr:
image: hotio/whisparr:nightly
container_name: whisparr
environment:
- PUID=1029 #CHANGE_TO_YOUR_UID
- PGID=65536 #CHANGE_TO_YOUR_GID
- TZ=America/Los_Angeles #CHANGE_TO_YOUR_TZ
- UMASK=022
volumes:
- /volume1/docker2/whisparr:/config
- /volume1/data/:/data
ports:
- 6969:6969/tcp
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,19 @@
version: '3.8'
services:
wizarr:
image: ghcr.io/wizarrrr/wizarr:latest
container_name: wizarr
environment:
- PUID=1029
- PGID=65536
- TZ=America/Los_Angeles
- DISABLE_BUILTIN_AUTH=false
volumes:
- /volume1/docker2/wizarr:/data/database
ports:
- 5690:5690/tcp
network_mode: synobridge
security_opt:
- no-new-privileges:true
restart: unless-stopped

View File

@@ -0,0 +1,18 @@
ssh-keygen -t ed25519 -C "synology@atlantis"
rsync -avhn --progress -e "ssh -T -c aes128-gcm@openssh.com -o Compression=no -x" \
"/volume1/data/media/tv/Lord of Mysteries/" \
root@100.99.156.20:/root/docker/plex/tvshows/
rsync -avh --progress -e "ssh -T -c aes128-gcm@openssh.com -o Compression=no -x" \
"/volume1/data/media/movies/Ballerina (2025)" \
root@100.99.156.20:/root/docker/plex/movies/
rsync -avh --progress -e "ssh -T -c aes128-gcm@openssh.com -o Compression=no -x" \
"/volume1/data/media/other/" \
--include 'VID_20240328_150621.mp4' \
--include 'VID_20240328_153720.mp4' \
--exclude '*' \
homelab@100.67.40.126:/home/homelab/whisper-docker/audio/

View File

@@ -0,0 +1,18 @@
# Baikal - CalDAV/CardDAV server
# Port: 8800
# Self-hosted calendar and contacts sync server
version: "3.7"
services:
baikal:
image: ckulka/baikal
container_name: baikal
ports:
- "12852:80"
environment:
- PUID=1026
- PGID=100
volumes:
- /volume2/metadata/docker/baikal/config:/var/www/baikal/config
- /volume2/metadata/docker/baikal/html:/var/www/baikal/Specific
restart: unless-stopped

View File

@@ -0,0 +1 @@
https://cal.vish.gg/dav.php/calendars/vish/default?export

View File

@@ -0,0 +1,20 @@
# Calibre Web - E-book management
# Port: 8083
# Web-based e-book library with OPDS support
name: calibre
services:
calibre-web:
container_name: calibre-webui
ports:
- 8183:8083
environment:
- PUID=1026
- PGID=100
- TZ=America/Los_Angeles
- DOCKER_MODS=linuxserver/mods:universal-calibre
- OAUTHLIB_RELAX_TOKEN_SCOPE=1
volumes:
- /volume2/metadata/docker/calibreweb:/config
- /volume2/metadata/docker/books:/books
restart: unless-stopped
image: ghcr.io/linuxserver/calibre-web

View File

@@ -0,0 +1,43 @@
# Cloudflare Tunnel for Atlantis NAS
# Provides secure external access without port forwarding
#
# SETUP INSTRUCTIONS:
# 1. Go to https://one.dash.cloudflare.com/ → Zero Trust → Networks → Tunnels
# 2. Create a new tunnel named "atlantis-tunnel"
# 3. Copy the tunnel token (starts with eyJ...)
# 4. Replace TUNNEL_TOKEN_HERE below with your token
# 5. In the tunnel dashboard, add these public hostnames:
#
# | Public Hostname | Service |
# |----------------------|----------------------------|
# | pw.vish.gg | http://localhost:4080 |
# | cal.vish.gg | http://localhost:12852 |
# | meet.thevish.io | https://localhost:5443 |
# | joplin.thevish.io | http://localhost:22300 |
# | mastodon.vish.gg | http://192.168.0.154:3000 |
# | matrix.thevish.io | http://192.168.0.154:8081 |
# | mx.vish.gg | http://192.168.0.154:8082 |
# | mm.crista.love | http://192.168.0.154:8065 |
#
# 6. Deploy this stack in Portainer
version: '3.8'
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflare-tunnel
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
network_mode: host # Needed to access localhost services and VMs
# Alternative if you prefer bridge network:
# networks:
# - tunnel_net
# extra_hosts:
# - "host.docker.internal:host-gateway"
# networks:
# tunnel_net:
# driver: bridge

View File

@@ -0,0 +1,83 @@
# Standalone DERP Relay Server — Atlantis (Home NAS)
# =============================================================================
# Tailscale/Headscale DERP relay for home-network fallback connectivity.
# Serves as region 902 "Home - Atlantis" in the headscale derpmap.
#
# Why standalone (not behind nginx):
# The DERP protocol does an HTTP→binary protocol switch inside TLS.
# It is incompatible with HTTP reverse proxies. Must handle TLS directly.
#
# Port layout:
# 8445/tcp — DERP relay (direct TLS, NOT proxied through NPM)
# 3480/udp — STUN (NAT traversal hints)
# Port 3478 taken by coturn/Jitsi, 3479 taken by coturn/Matrix on matrix-ubuntu.
#
# TLS cert:
# Issued by Let's Encrypt via certbot DNS challenge (Cloudflare).
# Cert path: /volume1/docker/derper-atl/certs/
# Cloudflare credentials: /volume1/docker/derper-atl/secrets/cloudflare.ini
# Auto-renewed monthly by the cert-renewer sidecar (ofelia + certbot/dns-cloudflare).
# On first deploy or manual renewal, run:
# docker run -it --rm \
# -v /volume1/docker/derper-atl/certs:/etc/letsencrypt \
# -v /volume1/docker/derper-atl/secrets:/root/.secrets:ro \
# certbot/dns-cloudflare certonly \
# --dns-cloudflare \
# --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
# -d derp-atl.vish.gg
# Then copy certs to flat layout:
# cp certs/live/derp-atl.vish.gg/fullchain.pem certs/live/derp-atl.vish.gg/derp-atl.vish.gg.crt
# cp certs/live/derp-atl.vish.gg/privkey.pem certs/live/derp-atl.vish.gg/derp-atl.vish.gg.key
#
# Firewall / DSM rules required (one-time):
# Allow inbound 8445/tcp and 3480/udp in DSM → Security → Firewall
#
# Router port forwards required (one-time, on home router):
# 8445/tcp → 192.168.0.200 (Atlantis LAN IP, main interface)
# 3480/udp → 192.168.0.200
#
# DNS: derp-atl.vish.gg → home public IP (managed by dynamicdnsupdater.yaml, unproxied)
# =============================================================================
services:
derper-atl:
image: fredliang/derper:latest
container_name: derper-atl
restart: unless-stopped
ports:
- "8445:8445" # DERP TLS — direct, not behind NPM
- "3480:3480/udp" # STUN (3478 taken by coturn/Jitsi, 3479 taken by coturn/Matrix)
volumes:
# Full letsencrypt mount required — live/ contains symlinks into archive/
# mounting only live/ breaks symlink resolution inside the container
- /volume1/docker/derper-atl/certs:/etc/letsencrypt:ro
environment:
- DERP_DOMAIN=derp-atl.vish.gg
- DERP_CERT_MODE=manual
- DERP_CERT_DIR=/etc/letsencrypt/live/derp-atl.vish.gg
- DERP_ADDR=:8445
- DERP_STUN=true
- DERP_STUN_PORT=3480
- DERP_HTTP_PORT=-1 # disable plain HTTP, TLS only
- DERP_VERIFY_CLIENTS=false # allow any node (headscale manages auth)
cert-renewer:
# Runs certbot monthly via supercronic; after renewal copies certs to the
# flat layout derper expects, then restarts derper-atl via Docker socket.
# Schedule: 03:00 on the 1st of every month.
image: certbot/dns-cloudflare:latest
container_name: derper-atl-cert-renewer
restart: unless-stopped
depends_on:
- derper-atl
entrypoint: >-
sh -c "
apk add --no-cache supercronic curl &&
echo '0 3 1 * * /renew.sh' > /crontab &&
exec supercronic /crontab
"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /volume1/docker/derper-atl/certs:/etc/letsencrypt
- /volume1/docker/derper-atl/secrets:/root/.secrets:ro
- /volume1/docker/derper-atl/renew.sh:/renew.sh:ro

View File

@@ -0,0 +1,28 @@
# Diun — Docker Image Update Notifier
#
# Watches all running containers on this host and sends ntfy
# notifications when upstream images update their digest.
# Schedule: Mondays 09:00 (weekly cadence).
#
# ntfy topic: https://ntfy.vish.gg/diun
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
LOG_LEVEL: info
DIUN_WATCH_WORKERS: "20"
DIUN_WATCH_SCHEDULE: "0 9 * * 1"
DIUN_WATCH_JITTER: 30s
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_NTFY_ENDPOINT: "https://ntfy.vish.gg"
DIUN_NOTIF_NTFY_TOPIC: "diun"
restart: unless-stopped
volumes:
diun-data:

View File

@@ -0,0 +1,20 @@
services:
dockpeek:
container_name: Dockpeek
image: ghcr.io/dockpeek/dockpeek:latest
healthcheck:
test: timeout 10s bash -c ':> /dev/tcp/127.0.0.1/8000' || exit 1
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
environment:
SECRET_KEY: "REDACTED_SECRET_KEY" # pragma: allowlist secret
USERNAME: vish
PASSWORD: REDACTED_PASSWORD # pragma: allowlist secret
DOCKER_HOST: unix:///var/run/docker.sock
ports:
- 3812:8000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: on-failure:5

View File

@@ -0,0 +1,71 @@
services:
db:
image: postgres:17
container_name: Documenso-DB
hostname: documenso-db
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "pg_isready", "-q", "-d", "documenso", "-U", "documensouser"]
timeout: 45s
interval: 10s
retries: 10
volumes:
- /volume1/docker/documenso/db:/var/lib/postgresql/data:rw
environment:
POSTGRES_DB: documenso
POSTGRES_USER: documensouser
POSTGRES_PASSWORD: "REDACTED_PASSWORD" # pragma: allowlist secret
restart: on-failure:5
documenso:
image: documenso/documenso:latest
container_name: Documenso
ports:
- 3513:3000
volumes:
- /volume1/docker/documenso/data:/opt/documenso:rw
depends_on:
db:
condition: service_healthy
environment:
- PORT=3000
- NEXTAUTH_SECRET="REDACTED_NEXTAUTH_SECRET" # pragma: allowlist secret
- NEXT_PRIVATE_ENCRYPTION_KEY=y6vZRCEKo2rEsJzXlQfgXg3fLKlhiT7h # pragma: allowlist secret
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=QA7tXtw7fDExGRjrJ616hDmiJ4EReXlP # pragma: allowlist secret
- NEXTAUTH_URL=https://documenso.thevish.io
- NEXT_PUBLIC_WEBAPP_URL=https://documenso.thevish.io
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://documenso:3000
- NEXT_PUBLIC_MARKETING_URL=https://documenso.thevish.io
- NEXT_PRIVATE_DATABASE_URL=postgres://documensouser:documensopass@documenso-db:5432/documenso
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documensouser:documensopass@documenso-db:5432/documenso
- NEXT_PUBLIC_UPLOAD_TRANSPORT=database
- NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
- NEXT_PRIVATE_SMTP_HOST=smtp.gmail.com
- NEXT_PRIVATE_SMTP_PORT=587
- NEXT_PRIVATE_SMTP_USERNAME=your-email@example.com
- NEXT_PRIVATE_SMTP_PASSWORD="REDACTED_PASSWORD" jkbo lmag sapq # pragma: allowlist secret
- NEXT_PRIVATE_SMTP_SECURE=false
- NEXT_PRIVATE_SMTP_FROM_NAME=Vish
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=your-email@example.com
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
#NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=true
#NEXT_PRIVATE_SMTP_APIKEY_USER=${NEXT_PRIVATE_SMTP_APIKEY_USER}
#NEXT_PRIVATE_SMTP_APIKEY=${NEXT_PRIVATE_SMTP_APIKEY}
#NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY}
#NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY}
#NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT}
#NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=${NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN}
#NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=${NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR}
#NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=${NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY}
#NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
#NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
#NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
#NEXT_PRIVATE_UPLOAD_ENDPOINT=${NEXT_PRIVATE_UPLOAD_ENDPOINT}
#NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=${NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE}
#NEXT_PRIVATE_UPLOAD_REGION=${NEXT_PRIVATE_UPLOAD_REGION}
#NEXT_PRIVATE_UPLOAD_BUCKET=${NEXT_PRIVATE_UPLOAD_BUCKET}
#NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=${NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID}
#NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=${NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY}
#NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
#NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}

View File

@@ -0,0 +1,19 @@
# DokuWiki - Wiki platform
# Port: 8084
# Simple wiki without database, uses plain text files
version: "3.9"
services:
dokuwiki:
image: ghcr.io/linuxserver/dokuwiki
container_name: dokuwiki
restart: unless-stopped
ports:
- "8399:80"
- "4443:443"
environment:
- TZ=America/Los_Angeles
- PUID=1026
- PGID=100
volumes:
- /volume2/metadata/docker/dokuwiki:/config

View File

@@ -0,0 +1,21 @@
# Dozzle - Real-time Docker log viewer
# Port: 8892
# Lightweight container log viewer with web UI
# Updated: 2026-03-11
services:
dozzle:
container_name: Dozzle
image: amir20/dozzle:latest
mem_limit: 3g
cpu_shares: 768
security_opt:
- no-new-privileges:true
restart: on-failure:5
ports:
- 8892:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /volume2/metadata/docker/dozzle:/data:rw
environment:
DOZZLE_AUTH_PROVIDER: simple
DOZZLE_REMOTE_AGENT: "100.72.55.21:7007,100.77.151.40:7007,100.103.48.78:7007,100.75.252.64:7007,100.67.40.126:7007,100.82.197.124:7007,100.125.0.20:7007,100.85.21.51:7007"

Some files were not shown because too many files have changed in this diff Show More