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

This commit is contained in:
Gitea Mirror Bot
2026-04-20 01:32:01 +00:00
commit e7652c8dab
1445 changed files with 364095 additions and 0 deletions

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