A fast, self-hosted homelab dashboard with drag-and-drop tiles and live widgets โ
one small container, no database, nothing phoning home.
๐ค Built with Claude. This project was designed and implemented with heavy help from Claude (Anthropic's AI). I'm not a professional developer โ I wanted a dashboard for my own homelab and used Claude as a pair-programmer to actually get it built.
I run a small homelab and wanted one good-looking start page for it โ a place to jump to my services and see live status at a glance: system load, Docker containers, DNS filtering, media, network, storage. I tried a few of the dashboards out there, and while they're solid projects, none of them quite gave me the exact mix of integrations and workflow I was after. So I built Dash# to be the dashboard that fit my setup โ configurable entirely from the browser, one small container, nothing else to babysit.
Under the hood it's a plain Node.js + Express app that serves a static frontend and proxies every integration server-side: the backend talks to your services with your stored credentials and returns small, normalized JSON. So there's no CORS, your tokens never reach the browser, and there's no database and no build step.
- ๐งฉ Design mode โ freely place & resize tiles on a grid, add/hide them from a catalog, rename section headings, and spread widgets across multiple pages; the layout is saved server-side
- ๐๏ธ Per-tile settings โ hover a tile, open โฏ โ Einstellungen: rename the tile, toggle its building blocks (rings, charts, summaries, posters, columns โฆ) and cap list lengths; stored with the dashboard layout
- โก Live widgets โ System (via Glances), Docker, AdGuard Home, JDownloader, Plex, UniFi Network & Protect, Nextcloud, Unraid, weather, news, game releases
- ๐ฐ News tile โ aggregates RSS/Atom feeds into one time-sorted list: pick sources from a curated IT/tech/hardware/gaming catalogue (split into German and English) or add your own feed URLs, then filter per tile by language, topic, source or keyword. Readability is adjustable per tile: text and thumbnail size, how many lines a headline and teaser may use, the spacing between headlines and how they are separated (divider line, cards or alternating tint). Clicking a headline opens a window with the teaser, the lead image and a link to the original article โ images are fetched and served by Dash# itself, so the browser never talks to the news sites
โถ๏ธ YouTube tile โ watch your favourite channels without a Google login: add channels by name (search needs the free YouTube Data API key) or by URL/@handle/ID, and Dash# polls each channel's public RSS feed for new uploads โ no API quota, nothing to sign into. New videos are flagged, counted in the tile badge and, if you opt in, raised as a browser notification; "mark as seen" sets the watermark per channel. On top of that, already-watched videos are tracked per video โ playing one in the window or opening it on YouTube marks it watched (you can also toggle the tick by hand), watched videos drop out of the tile's "new" count and are hidden from the list by default (a per-tile toggle brings them back, greyed out). A per-tile time filter (all / today / last 24 h / 7 days) keeps the feed to just the fresh uploads. An anti-bubble discovery section deliberately surfaces content from outside your subscriptions โ a curated pool of diverse, German-language topic channels (no key) plus a slow topic-rotation over your own search terms, biased to German results (key). Smart filters (hide Shorts, min/max duration, keyword allow/block, include or drop live & premieres) act on both the feed and the notifications; channels can be grouped into topic tabs; a dashboard-local watch-later queue lives inconfig/youtube.json(independent of YouTube). Clicking a video opens a window with a description, a watch-later button and SponsorBlock segment info; an opt-in privacy player (youtube-nocookie) plays it inside the dashboard, otherwise you jump out to YouTube. Thumbnails and avatars are proxied by Dash# itself, so the browser only talks to the dashboard (the embedded player is the sole, opt-in exception)- ๐ฎ Game releases tile โ shows the games released on a given day, sourced from IGDB: box art, platform chips, genres and critic score per card. Step through days with โน/โบ, jump to any date with the date picker, or open the magnifier to look up when a specific game comes out. Clicking a card opens a window with the German description (taken from the Steam store, falling back to German Wikipedia, and marked
ENwhen only the English IGDB text exists), ratings incl. USK/PEGI, developer, publisher, the Steam price (with the discount spelled out; absent for titles without a Steam page), game modes, engine, screenshots and store links. Metadata is translated to German by lookup tables in the module. Games that are in the Xbox Game Pass catalogue right now carry a Game Pass chip next to their platform chips โ no credentials needed, the catalogue is public โ and the detail window spells out whether that is console, PC or via EA Play. A relevance filter keeps the daily shovelware out (โ 8 of โ 54 titles a day at the default setting); platform, "only with cover" and "only Game Pass" filters are per tile. Images are proxied by Dash# itself, so the browser never talks to IGDB or Steam - ๐ฅ๏ธ Unraid suite โ eight tiles on the official GraphQL API: VMs (incl. VNC console), Docker containers (start/stop/restart), array & parity (status, capacity, check control), per-disk health, shares, notifications (incl. archive), system info (live CPU/RAM, versions, reboot/shutdown via SSH) and UPS โ risky actions locked behind a server-side opt-in
- ๐ข Service monitoring โ the Service Status tile checks your services and shows online/offline + latency; the check method is picked automatically from what you enter: a URL (
http(s)://โฆ) โ HTTP,host:portโ TCP connect, a bare hostname or IP โ ICMP ping - โ๏ธ Configure in the browser โ everything under
/settings, no config files to hand-edit - ๐ Private by design โ no telemetry, no tracking, and no third-party requests from the browser: fonts ship with the image and service icons are proxied and cached by Dash# itself, so the dashboard works fully offline; secrets stay in your mounted config volume
- ๐ณ One container โ
node:20-alpine, multi-arch (amd64/arm64), healthcheck, ~48 MB
Note
Dash# is still pre-1.0. It works and is used daily, but expect rough edges and breaking changes before 1.0. Feedback and issues are very welcome.
Coming soon.
docker run -d --name dashsharp \
-p 8085:3000 \
-v /path/to/appdata/dashsharp:/app/config \
even512/dashsharp:latestOpen http://<host>:8085, then go to โ๏ธ โ Integrations to connect your services. On first start a
default config is written into the mounted volume automatically.
Prefer Compose? See docker-compose.yml:
docker compose up -d- Fetch the template on your Unraid box:
wget -O /boot/config/plugins/dockerMan/templates-user/my-dashsharp.xml \ https://raw.githubusercontent.com/even512/dashsharp/main/unraid/dashsharp.xml
- Docker โ Add Container โ pick Dash-Sharp from the Template dropdown.
- Check the WebUI port (
8085) and appdata path (/mnt/user/appdata/dashsharp) โ Apply.
Everything is configured from the web UI under Settings โ Integrations and stored in
config/secrets.json inside your mounted volume โ never baked into the image, never committed.
| Integration | Needs |
|---|---|
| System / Docker | Glances URL (http://host:61208) |
| AdGuard Home | URL, user, password |
| JDownloader | MyJDownloader email + password (+ optional device name) |
| Plex | URL, X-Plex-Token |
| UniFi | Cloud API key (api.ui.com) |
| Nextcloud | URL, user, app password |
| Unraid | Unraid URL + GraphQL API key |
| Weather | City (Open-Meteo, no key) |
The tile reads release dates from IGDB, which authenticates through Twitch. Both values are free and take about five minutes to create:
- Open https://dev.twitch.tv/console/apps and click Register Your Application
- OAuth redirect URL
http://localhost, category Application Integration - Copy the Client ID and generate a Client Secret
Enter them under Settings โ Modules โ Game Releases, or as environment variables:
IGDB_CLIENT_ID=
IGDB_CLIENT_SECRET=There is no monthly request cap; only the rate is limited (4 requests/second), which the module keeps to. Until both values are set, the tile stays in its "not configured" state and the server makes no outbound request at all.
IGDB carries no price data at all, so the price in the detail window comes from
the public Steam store API โ no extra credentials, and no extra request either:
the German description already fetches that same store page via the Steam AppID
IGDB hands out in external_games, and the price falls out of the same
response. Amounts are in EUR (cc=de) and cached for three hours, while the
IGDB detail keeps its 24 hours โ a discount that ended hours ago would be worse
than no price at all.
The price is therefore Steam's. Console-only titles, releases not yet priced and anything without a Steam page simply show no price row, and a Steam outage costs that one row, never the window. Nothing is estimated or carried over from another store.
node scripts/igdb-check.mjs verifies the credentials and prints what IGDB
returns for a given day โ useful when the tile looks emptier than expected:
node scripts/igdb-check.mjs # today
node scripts/igdb-check.mjs 2026-09-17 # a specific day
node scripts/igdb-check.mjs --search gothic # test the magnifier
node scripts/igdb-check.mjs --game 375232 # detail view incl. translation
node scripts/igdb-check.mjs --gamepass # Game Pass catalogue (no credentials)Microsoft has no official Game Pass API, but the catalogue behind
catalog.gamepass.com and displaycatalog.mp.microsoft.com is public, so this
part needs no setup and no credentials. The full catalogue (~900 titles) is
fetched at most every six hours, kept in memory only, and served for up to a week
if Microsoft is unreachable โ an outage costs you a chip, never the tile.
Two limits are deliberate:
- There is no "coming to Game Pass on X". Every collection the Xbox site itself uses was checked; none of them lists announced titles, and even "day one releases" only contains games that have already shipped. The tile therefore only ever reflects the current catalogue.
- Titles are matched exactly, after normalising trademarks, punctuation and
platform suffixes (
Xbox Series X|S,for PC,(Game Preview)โฆ). Edition names are not stripped: the catalogue carries Halo Wars: Definitive Edition but not Halo Wars, and IGDB knows both as separate games โ folding them together would put the chip on a game that is not in Game Pass. So a game can be in Game Pass without showing a chip; it will never show one without being in it.--gamepass "Some Game"shows which key a title normalises to.
Eight tiles cover the whole Unraid GraphQL API (Unraid 7.2+) โ add them from the tile catalog in design mode:
| Tile | Shows | Controls |
|---|---|---|
| Unraid VMs | live VM status | start / stop / pause / resume / reboot / force-stop, VNC/RDP console |
| Unraid Docker | containers, uptime, pending updates | start / stop / restart / pause |
| Unraid Array | array state, capacity, parity status & progress | array start/stop, parity check start/pause/resume/cancel |
| Unraid Disks | per-disk status, temperature, fill level, error counter | โ |
| Unraid Shares | per-share usage with fill bars | โ |
| Unraid Meldungen | unread notifications by severity | archive one / all |
| Unraid System | live CPU/RAM, uptime, versions, host info | reboot / shutdown |
| Unraid USV | UPS charge, runtime, load/power | โ |
Tiles marked 403 while it's off), destructive actions additionally ask for
confirmation in the browser, and reboot/shutdown also requires the SSH access below (the GraphQL
API has no reboot/shutdown mutation). Only enable this if the dashboard is not publicly reachable.
Setup:
- Enable the API and create a key on your Unraid box:
(or generate one from the Unraid web UI). A viewer-scoped key is enough for display; controlling VMs/containers/array needs the matching write scopes.
unraid-api apikey --create
- In Settings โ Unraid, enter your Unraid URL (e.g.
http://tower.local) and the API key.
Fields that only newer unraid-api versions expose (live metrics, parity status, UPS power data,
container update flags) degrade gracefully on older servers โ the tiles simply omit them.
By default the VNC button opens Unraid's own VM manager, which sits behind the Unraid web login.
To jump straight into the console โ embedded in the dashboard, with no login โ add SSH access
in the same settings tab (host defaults to the Unraid URL, user defaults to root; password or
private key). Dash# then reads the VM's VNC port over SSH (virsh dumpxml), tunnels the connection,
and renders it with a bundled noVNC โ talking directly to the VM's
QEMU VNC port, bypassing Unraid's login entirely. The console modal also has open-in-new-tab and
fullscreen buttons.
Warning
This gives Dash# SSH access to your Unraid box โ only enable it on a trusted LAN. If you serve
Dash# over HTTPS, browsers block the plain ws:// VNC connection (mixed content); run Dash#
over HTTP on your LAN for the embedded console. Per-VM CPU/RAM graphs are not exposed by the
GraphQL API and are therefore not shown.
VNC is inherently choppy for interactive desktops (it re-encodes the screen on the host), so the same lag shows up in Unraid's own noVNC too. Options, from quick to best:
- Tune noVNC โ the console toolbar has a quality selector (Fluid / Balanced / Sharp); Fluid sends less data and feels smoother. Dash# also requests a server-side resize to your window and disables Nagle on the socket to cut input latency.
- Windows guests โ RDP. Windows VMs get an extra RDP button that downloads a ready-made
.rdpfile for the native client โ dramatically smoother than VNC. The guest IP is found automatically over SSH (virsh domifaddr; install the VirtIO guest tools / QEMU guest agent), or set it per-VM under Settings โ Unraid โ VMs & RDP. Enable Remote Desktop inside Windows first. The Windows type is auto-detected from the Unraid VM template and can be overridden there. - Linux guests โ switch the VM's graphics from QXL to VirtIO (3D / VirGL) and install the guest drivers; give it more video RAM.
- Near-native (gaming / GPU) โ run Sunshine in the VM and connect with Moonlight for hardware-encoded H.264/HEVC streaming, ideal with GPU passthrough.
Any value can also be set as an environment variable (see .env.example); env vars take
precedence over the UI values.
Docker tile shows only running containers? That's controlled by Glances itself, not Dash#. Set
all = Trueunder[containers](or the legacy[docker]section) inglances.confon your Glances host and restart Glances to include stopped/exited containers too.
System panel shows a container ID instead of your hostname? This happens when the Glances container isn't run with
--uts=host, so it reports its own Docker-assigned hostname. Fix it at the source (--uts=host), or set a fixed override under Settings โ Glances โ Hostname.
All mutable state lives in the mounted /app/config volume:
config/
โโโ services.yaml
โโโ secrets.json
โโโ dashboard-layout.json
โโโ quicklinks.json
โโโ disks.json
โโโ news.json
โโโ status.json
services.yaml holds title, search and quicklinks and is auto-created on first run. secrets.json
holds your API keys/tokens, set via the UI. The rest track dashboard state: tile order and visibility,
quick-access tiles, custom disk names, the selected news feeds, and health-check targets (URL,
host:port or a bare host/IP).
Back up that folder and you've backed up everything. Image updates never touch it.
- Unraid: Docker tab โ Check for Updates โ apply. Your config is preserved.
- Compose:
docker compose pull && docker compose up -d.
Dash# is a single Node/Express process. It serves the static frontend from public/ and exposes a set
of same-origin /api/* endpoints the browser calls. Each integration is a server-side proxy: the
backend contacts Glances/AdGuard/Plex/etc. with your stored credentials and returns a trimmed JSON โ no
CORS, no tokens in the browser. State is a handful of JSON/YAML files in the config volume; there is no
database and no build step.
The dashboard has no built-in authentication โ anyone who can reach the port can use it. Keep it on your LAN, or put it behind a VPN or an authenticated reverse proxy. Do not expose port 8085 directly to the internet.
Two things Dash# does do on its own:
- Host allowlist. Requests are only accepted when the
Hostheader looks like a LAN address (localhost, private IP ranges,.local/.lan/.home/.internal). This blocks DNS rebinding โ an attacker domain resolving to your LAN IP would otherwise be same-origin and could trigger the control actions. Behind a reverse proxy with its own hostname, add it toTRUSTED_HOSTS(comma-separated);TRUSTED_HOSTS=*turns the check off. - Non-root container. The entrypoint starts as root, adopts the ownership of your mounted
/app/config(so it always matches your volume โ no permission surprises on existing installs) and drops privileges before starting the app. Override withPUID/PGIDif you want a specific user; if the volume turns out not to be writable, it stays root and says so in the log rather than dying. Note that unprivileged ICMP needssysctl -w net.ipv4.ping_group_range="0 2147483647"on the host; without it, the Service Status tile falls back to a TCP reachability check for bare hostnames (HTTP andhost:porttargets are unaffected).
git clone https://github.com/even512/dashsharp.git
cd dashsharp
cp .env.example .env
npm install
npm run devnpm run dev starts the app at http://localhost:3000. The .env copy is optional.
Run the checks (syntax, module manifests, core endpoints, host allowlist):
npm testBuild the image yourself: docker build -t dashsharp .
Regenerate the logo/icons after editing logo.svg:
npm i -D sharp && node scripts/render-icon.mjsDash# is built so a new integration is two files โ no changes to server.js, app.js or
index.html, and no build step:
server/modules/<id>.js fetch, normalize, cache, push
public/modules/<id>.js render the tile
The backend file declares what it needs and how to get it:
module.exports = {
id: 'uptimekuma',
label: 'Uptime Kuma',
ttl: 30000,
secrets: [{ key: 'KUMA_URL', label: 'URL' },
{ key: 'KUMA_TOKEN', label: 'API token', masked: true }],
configured: (get) => !!get('KUMA_URL'),
async fetch(get, ctx) {
const d = await ctx.httpJson(`${get('KUMA_URL')}/api/status`);
return { ok: true, up: d.up, down: d.down };
},
};From that, the registry derives the cache slot, the TTL, GET /api/uptimekuma, the SSE push event,
the entries in /api/secrets (incl. masking), in-flight de-duplication, the not_configured
response and a _stale fallback to the last good data when the upstream is down.
The frontend file registers the tile โ size, options, live event and markup in one manifest โ and the tile catalog, per-tile settings, push routing and settings entry follow from it.
Both directories carry a README.md with the full contract and a commented _example.js template
(files starting with _ are not loaded). npm test validates every manifest, so a typo fails the
build instead of silently producing a blank tile.
This started as a personal project, so it's still a bit rough around the edges โ but issues and PRs are very welcome. Small, focused improvements are the easiest for me to review and land.
No license chosen yet (default copyright applies). An OSI license such as MIT may be added later.