lscr.io is a mirror of ghcr.io — same manifests and auth service. Using the ghcr.io token flow (with GITHUB_TOKEN) is reliable; calling the lscr.io token endpoint with service=lscr.io silently fails.
syco.me — Homelab Dashboard
A personal homelab dashboard built with React + TypeScript on the frontend and an Express proxy server on the backend. Deployed via Woodpecker CI to a Proxmox LXC container.
Features
- Dashboard — live widgets for all homelab services
- Apps — icon grid of all self-hosted apps with local icons (no CDN)
- Home — mobile-optimised page with weather and public transit departures
- Responsive — desktop defaults to Dashboard, mobile defaults to Home
- Collapsible sections — click any section label to fold it away
- Header — live clock, weather summary, and navigation tabs
Stack
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite |
| Backend | Node.js, Express |
| Routing | React Router v6 |
| Fonts | Syne, JetBrains Mono (Google Fonts) |
| CI/CD | Woodpecker CI |
| Deployment | Docker on Proxmox LXC |
Widgets
Infrastructure
| Widget | Service | API |
|---|---|---|
| Proxmox | Proxmox VE | REST API (API Token) |
| Synology NAS | DSM | Synology Web API |
| AdGuard Home | AdGuard | REST API (Basic Auth) |
| Headscale | Headscale VPN | REST API (API Key) |
| FritzBox | AVM FRITZ!Box | TR-064 SOAP API |
Media
| Widget | Service | API |
|---|---|---|
| Jellyfin | Jellyfin | REST API (API Key) |
| Navidrome | Navidrome | Subsonic API |
| RomM | RomM | REST API |
| Arr Calendar | Sonarr + Radarr | REST API |
| Arr Stats | Sonarr + Radarr + Lidarr | REST API |
| qBittorrent | qBittorrent | Web API |
Monitoring
| Widget | Service | API |
|---|---|---|
| Uptime Kuma | Kuma | REST API |
| CrowdSec | CrowdSec LAPI | REST API (Bouncer Key) |
| Grafana | Grafana | REST API |
| Prometheus | Prometheus | REST API |
| Loki | Loki | REST API |
Access
| Widget | Service | API |
|---|---|---|
| Authentik | Authentik | REST API (API Token) |
| Vaultwarden | Vaultwarden | Admin API |
Home (mobile)
| Widget | Source |
|---|---|
| Weather | wttr.in — no API key needed |
| Transit | VAG PULS API — no API key needed |
Project Structure
├── server/
│ ├── index.ts # Express server + route registration
│ └── routes/ # One file per service proxy
├── src/
│ ├── components/
│ │ ├── Header.tsx # Nav, clock, weather summary
│ │ ├── AppsSection.tsx # Icon tile grid
│ │ └── widgets/ # One component per dashboard widget
│ ├── config/
│ │ ├── apps.ts # App links + icon paths (edit order here)
│ │ └── dashboard.ts # Dashboard sections + widget order (edit order here)
│ ├── hooks/
│ │ └── useIsMobile.ts
│ ├── pages/
│ │ ├── AppsPage.tsx
│ │ └── MobileHome.tsx # Weather + transit for mobile
│ └── App.tsx # Routing
├── public/
│ └── icons/ # All icons stored locally
├── Dockerfile
└── .woodpecker.yml
To change widget/app order: edit
src/config/dashboard.ts(widgets) orsrc/config/apps.ts(app tiles) — no need to touchApp.tsx.
Environment Variables
Copy .env.example to .env and fill in your values.
# Proxmox VE
PROXMOX_HOST=https://192.168.178.x:8006
PROXMOX_TOKEN=PVEAPIToken=user@realm!tokenid=...
# Synology DSM
SYNOLOGY_HOST=http://192.168.178.x:5000
SYNOLOGY_USER=...
SYNOLOGY_PASSWORD=...
# AdGuard Home
ADGUARD_HOST=http://192.168.178.x
ADGUARD_USER=...
ADGUARD_PASSWORD=...
# CrowdSec
CROWDSEC_HOST=http://192.168.178.x:8080
CROWDSEC_API_KEY=...
# Headscale
HEADSCALE_HOST=http://192.168.178.x:8085
HEADSCALE_API_KEY=...
# FRITZ!Box
FRITZBOX_HOST=http://192.168.178.1
# Uptime Kuma
KUMA_HOST=http://192.168.178.x:3001
KUMA_USER=...
KUMA_PASSWORD=...
# Authentik
AUTHENTIK_HOST=http://192.168.178.x:9000
AUTHENTIK_TOKEN=...
# Vaultwarden
VAULTWARDEN_HOST=http://192.168.178.x:8087
VAULTWARDEN_ADMIN_TOKEN=...
# qBittorrent
QBT_HOST=http://192.168.178.x:8080
QBT_USER=...
QBT_PASSWORD=...
# *arr suite
RADARR_HOST=http://192.168.178.x:7878
RADARR_API_KEY=...
SONARR_HOST=http://192.168.178.x:8989
SONARR_API_KEY=...
LIDARR_HOST=http://192.168.178.x:8686
LIDARR_API_KEY=...
# Jellyfin
JELLYFIN_HOST=http://192.168.178.x:8096
JELLYFIN_API_KEY=...
# Navidrome
NAVIDROME_HOST=http://192.168.178.x:4533
NAVIDROME_USER=...
NAVIDROME_PASSWORD=...
# RomM
ROMM_HOST=http://192.168.178.x:7998
ROMM_USER=...
ROMM_PASSWORD=...
# Grafana
GRAFANA_HOST=http://192.168.178.x:3000
GRAFANA_TOKEN=
# Prometheus
PROMETHEUS_HOST=http://192.168.178.x:9090
# Loki
LOKI_HOST=http://192.168.178.x:3100
# Weather (wttr.in — no API key needed)
WEATHER_LOCATION='90461 Nürnberg'
# Transit (VAG PULS API — no API key needed)
TRANSIT_STOP_VAG=SCHW
# Server port
PORT=3001
Note on special characters in passwords: The
.envfile supports shell-quoted values ('...'or"..."). The deployment script strips quotes before passing to Docker via--env-file.
Local Development
npm install
npm run dev
Runs Vite (port 5173) and the Express server (port 3001) concurrently.
Deployment
Deployment is fully automated via Woodpecker CI on every push to main.
The pipeline (.woodpecker.yml):
- Builds a Docker image from
Dockerfile - Strips shell quotes from
.env(Docker--env-filepasses values literally) - Stops and removes the old container
- Starts a new container with
docker run --env-file
The .env file lives at /opt/docker/dashboard/.env on the host and is mounted into the pipeline as a secret volume — it is never committed to the repository.
Adding a New App Link
Edit src/config/apps.ts:
{ name: 'My App', url: 'https://myapp.syco.me', icon: `${ICONS}/myapp.svg` }
Download the icon to public/icons/ first. Icons are sourced from selfh.st/icons.
Adding a New Widget
- Create
src/components/widgets/MyWidget.tsx - Add it to
src/config/dashboard.tsin the appropriate section
Changing the Transit Stop
Update TRANSIT_STOP_VAG in .env with a VAG stop ID. Look up stop IDs via:
https://start.vag.de/dm/api/haltestellen.json/vag?name=<stop name>