From 3ccffd67abe1e0b4c5cbd656ffebab632f358b0f Mon Sep 17 00:00:00 2001
From: SandObserver <260779319+SandObserver@users.noreply.github.com>
Date: Sat, 5 Sep 2026 18:36:14 -0400
Subject: [PATCH] Docs: add a dashboard comparison page
Add /docs/is-stackyard-for-you/, comparing Stackyard with gethomepage, Dashy
and Homarr on settings, custom API, health checks, self-hosted integrations,
user accounts and runtime dependencies. Every figure is counted from each
project's own documentation and source, and the method is stated in a footnote.
Add CompareTable, a fixed-layout table that marks the Stackyard column, carries
numbered footnotes with anchors, and scrolls with a group role when the content
column is too narrow.
Add CalmContrast, a figure holding two panels: a generic dashboard printing
labelled counts for eight services, and the real Stackyard widgets served from
public/widgets beside an app grid whose badges carry the same values. The
panels stack on a container query, since the content column follows the
sidebar rather than the viewport.
Add preview data for a Nightly Backup card and three Now Playing sessions, one
picked per page load, and vendor the Immich mark alongside the existing icons.
---
astro.config.mjs | 1 +
public/api/widget-config/cc-backup | 1 +
public/api/widget-config/cc-np-a | 1 +
public/api/widget-config/cc-np-b | 1 +
public/api/widget-config/cc-np-c | 1 +
public/api/widget-data/cc-backup | 12 +
public/api/widget-data/cc-np-a | 6 +
public/api/widget-data/cc-np-b | 6 +
public/api/widget-data/cc-np-c | 6 +
public/icons/immich.svg | 1 +
src/components/CalmContrast.astro | 484 ++++++++++++++++++
src/components/CompareTable.astro | 274 ++++++++++
.../docs/docs/is-stackyard-for-you.mdx | 82 +++
src/styles/docs.css | 6 +-
14 files changed, 881 insertions(+), 1 deletion(-)
create mode 100644 public/api/widget-config/cc-backup
create mode 100644 public/api/widget-config/cc-np-a
create mode 100644 public/api/widget-config/cc-np-b
create mode 100644 public/api/widget-config/cc-np-c
create mode 100644 public/api/widget-data/cc-backup
create mode 100644 public/api/widget-data/cc-np-a
create mode 100644 public/api/widget-data/cc-np-b
create mode 100644 public/api/widget-data/cc-np-c
create mode 100644 public/icons/immich.svg
create mode 100644 src/components/CalmContrast.astro
create mode 100644 src/components/CompareTable.astro
create mode 100644 src/content/docs/docs/is-stackyard-for-you.mdx
diff --git a/astro.config.mjs b/astro.config.mjs
index c69ac85..790f1bc 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -82,6 +82,7 @@ export default defineConfig({
},
sidebar: [
{ label: 'Introduction', link: '/docs/' },
+ { label: 'Compare dashboards', link: '/docs/is-stackyard-for-you/' },
{
label: 'Installation',
items: [
diff --git a/public/api/widget-config/cc-backup b/public/api/widget-config/cc-backup
new file mode 100644
index 0000000..c7801f0
--- /dev/null
+++ b/public/api/widget-config/cc-backup
@@ -0,0 +1 @@
+{"widgetSize":"small","widgetConfig":{"slots":[{"provider":"duplicati","customName":"Nightly Backup","jobId":"1","dupUrl":"https://duplicati.example.com"}]}}
diff --git a/public/api/widget-config/cc-np-a b/public/api/widget-config/cc-np-a
new file mode 100644
index 0000000..cc8eb0b
--- /dev/null
+++ b/public/api/widget-config/cc-np-a
@@ -0,0 +1 @@
+{"widgetSize": "small", "widgetConfig": {"provider": "jellyfin", "jellyfinUrl": "https://media.example.com", "href": "https://media.example.com"}}
\ No newline at end of file
diff --git a/public/api/widget-config/cc-np-b b/public/api/widget-config/cc-np-b
new file mode 100644
index 0000000..cc8eb0b
--- /dev/null
+++ b/public/api/widget-config/cc-np-b
@@ -0,0 +1 @@
+{"widgetSize": "small", "widgetConfig": {"provider": "jellyfin", "jellyfinUrl": "https://media.example.com", "href": "https://media.example.com"}}
\ No newline at end of file
diff --git a/public/api/widget-config/cc-np-c b/public/api/widget-config/cc-np-c
new file mode 100644
index 0000000..cc8eb0b
--- /dev/null
+++ b/public/api/widget-config/cc-np-c
@@ -0,0 +1 @@
+{"widgetSize": "small", "widgetConfig": {"provider": "jellyfin", "jellyfinUrl": "https://media.example.com", "href": "https://media.example.com"}}
\ No newline at end of file
diff --git a/public/api/widget-data/cc-backup b/public/api/widget-data/cc-backup
new file mode 100644
index 0000000..c960711
--- /dev/null
+++ b/public/api/widget-data/cc-backup
@@ -0,0 +1,12 @@
+[
+ {
+ "id": "demo-0",
+ "name": "Nightly Backup",
+ "provider": "duplicati",
+ "status": "healthy",
+ "lastFinished": "2026-09-05T02:15:00.000Z",
+ "nextRun": "2026-09-06T02:15:00.000Z",
+ "size": "128.4 GB",
+ "href": ""
+ }
+]
diff --git a/public/api/widget-data/cc-np-a b/public/api/widget-data/cc-np-a
new file mode 100644
index 0000000..9ac40bd
--- /dev/null
+++ b/public/api/widget-data/cc-np-a
@@ -0,0 +1,6 @@
+{
+ "provider": "jellyfin",
+ "sessions": [
+ { "title": "The Dark Knight Rises", "subtitle": "2012 · 2160p", "progress": 0.641, "state": "playing", "type": "movie", "player": "Living Room TV" }
+ ]
+}
diff --git a/public/api/widget-data/cc-np-b b/public/api/widget-data/cc-np-b
new file mode 100644
index 0000000..8b3b341
--- /dev/null
+++ b/public/api/widget-data/cc-np-b
@@ -0,0 +1,6 @@
+{
+ "provider": "jellyfin",
+ "sessions": [
+ { "title": "Guardians of the Galaxy", "subtitle": "2014 · 2160p", "progress": 0.884, "state": "playing", "type": "movie", "player": "Study Desktop" }
+ ]
+}
diff --git a/public/api/widget-data/cc-np-c b/public/api/widget-data/cc-np-c
new file mode 100644
index 0000000..1564c00
--- /dev/null
+++ b/public/api/widget-data/cc-np-c
@@ -0,0 +1,6 @@
+{
+ "provider": "jellyfin",
+ "sessions": [
+ { "title": "House of the Dragon", "subtitle": "S1 E7 · 1080p", "progress": 0.455, "state": "playing", "type": "episode", "player": "Bedroom Tablet" }
+ ]
+}
diff --git a/public/icons/immich.svg b/public/icons/immich.svg
new file mode 100644
index 0000000..5c758ff
--- /dev/null
+++ b/public/icons/immich.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/CalmContrast.astro b/src/components/CalmContrast.astro
new file mode 100644
index 0000000..39035b7
--- /dev/null
+++ b/src/components/CalmContrast.astro
@@ -0,0 +1,484 @@
+---
+/* Left: the shape most dashboards take, a widget per service, each one a box of
+ labelled counts. Generic, not a copy of any product. Right: the real Stackyard
+ widgets, served from public/widgets, on the product's own surface. Geometry
+ mirrors WidgetPreview.astro and must stay in step with it. */
+
+const CARDS = [
+ { name: 'Jellyfin', sub: 'Media server', state: 'RUNNING',
+ stats: [['412', 'MOVIES'], ['38', 'SERIES'], ['1,204', 'EPISODES'], ['0', 'SONGS']] },
+ { name: 'Homebridge', sub: 'HomeKit bridge', state: 'RUNNING',
+ stats: [['0', 'UPDATES'], ['0', 'ERRORS'], ['1', 'PAIRED'], ['1/1', 'BRIDGES']] },
+ { name: 'AdGuard Home', sub: 'DNS filter', state: 'HEALTHY',
+ stats: [['12,059', 'QUERIES'], ['3,742', 'BLOCKED'], ['0', 'PARENTAL'], ['0', 'MALWARE']] },
+ { name: 'Kopia', sub: 'Backup and snapshots', state: 'HEALTHY',
+ stats: [['OK', 'STATUS'], ['128 GB', 'SIZE'], ['02:15', 'LAST RUN'], ['02:15', 'NEXT RUN']] },
+ { name: 'Seerr', sub: 'Request manager', state: 'RUNNING',
+ stats: [['7', 'PENDING'], ['112', 'APPROVED'], ['964', 'AVAILABLE'], ['88', 'COMPLETED']] },
+ { name: 'Proxy Manager', sub: 'Reverse proxy', state: 'UNHEALTHY', bad: true,
+ stats: [['-', 'ENABLED'], ['-', 'DISABLED'], ['-', 'TOTAL'], ['-', 'ERRORS']] },
+ { name: 'Portainer', sub: 'Container manager', state: 'RUNNING',
+ stats: [['38', 'RUNNING'], ['0', 'STOPPED'], ['38', 'TOTAL'], ['11%', 'CPU']] },
+ { name: 'Immich', sub: 'Photo and video library', state: 'RUNNING',
+ stats: [['1', 'USERS'], ['2,182', 'PHOTOS'], ['213', 'VIDEOS'], ['94 GB', 'STORAGE']] },
+];
+
+/* Every badge points at a count the left panel prints. */
+const ROW_1 = [
+ { name: 'Homebridge', icon: '/icons/homebridge.svg', bg: '#3d1854' },
+ { name: 'AdGuard', icon: '/icons/pi-hole.svg', bg: '#f2f2f7', badge: '31%', badgeBg: '#0091ff', badgeInk: '#ffffff' },
+ { name: 'Portainer', icon: '/icons/portainer.svg', bg: '#1c1c1e' },
+];
+
+const ROW_2 = [
+ { name: 'Seerr', icon: '/icons/seerr.svg', bg: '#1c1c1e', badge: '7', badgeBg: '#ffcc00', badgeInk: '#1c1c1e' },
+ { name: 'Proxy', icon: '/icons/npm.svg', bg: '#1c1c1e', badge: '!', badgeBg: '#ff4245', badgeInk: '#ffffff' },
+ { name: 'Immich', icon: '/icons/immich.svg', bg: '#f2f2f7' },
+];
+
+const DESIGN = 170;
+const CARD_W = 110;
+const CARD_H = 108.1;
+const RADIUS = 20.2;
+const scale = Math.max(CARD_W / DESIGN, CARD_H / DESIGN);
+const tx = (CARD_W - DESIGN * scale) / 2;
+const ty = (CARD_H - DESIGN * scale) / 2;
+const frameStyle = `width:${DESIGN}px;height:${DESIGN}px;transform:translate(${tx}px,${ty}px) scale(${scale});`;
+const cardStyle = `width:${CARD_W}px;height:${CARD_H}px;border-radius:${RADIUS}px;`;
+---
+
+ The usual approach Everything is equally loud. Stackyard Only what matters to you.
+ {t.badge && (
+
+ {t.badge}
+
+ )}
+
+ {t.name}
+
+ ))}
+
+ {t.badge && (
+
+ {t.badge}
+
+ )}
+
+ {t.name}
+
+ ))}
+
| Feature | +
+ |
+ {rivals.map((name) => (
+ {name} | + ))} +
|---|---|---|
| + {row.label} + {row.labelNote && ( + + {row.labelNote} + See note {row.labelNote} + + )} + | + {row.values.map((value, i) => ( ++ {value} + {i === 0 && row.cellNote && ( + + {row.cellNote} + See note {row.cellNote} + + )} + | + ))} +