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

+
+
+
+ +
+ {ROW_1.map((t) => ( + + + + {t.badge && ( + + {t.badge} + + )} + + {t.name} + + ))} +
+ +
+ {ROW_2.map((t) => ( + + + + {t.badge && ( + + {t.badge} + + )} + + {t.name} + + ))} +
+
+

Only what matters to you.

+
+
+ +
+ The same facts either way: what is playing, last night's backup, seven + pending requests, thirty-one percent of queries blocked and a proxy that + needs attention. +
+
+ + + + diff --git a/src/components/CompareTable.astro b/src/components/CompareTable.astro new file mode 100644 index 0000000..7f68ae0 --- /dev/null +++ b/src/components/CompareTable.astro @@ -0,0 +1,274 @@ +--- +interface Row { + label: string; + values: [string, string, string, string]; + /* 1-based index into `footnotes`, marked on the Stackyard cell. */ + cellNote?: number; + /* 1-based index into `footnotes`, marked on the row label. */ + labelNote?: number; +} + +interface Props { + rows: Row[]; + rivals: string[]; + /* Rendered as HTML so a note can link. Authored here, never user input. */ + footnotes: string[]; +} + +const { rows, rivals, footnotes } = Astro.props; +--- + +
+
+ + + + + + + {rivals.map((name) => ( + + ))} + + + + {rows.map((row) => ( + + + {row.values.map((value, i) => ( + + ))} + + ))} + +
Stackyard compared with {rivals.join(', ')}
Feature + + + {name}
+ {row.label} + {row.labelNote && ( + + {row.labelNote} + See note {row.labelNote} + + )} + + {value} + {i === 0 && row.cellNote && ( + + {row.cellNote} + See note {row.cellNote} + + )} +
+
+ +
    + {footnotes.map((text, i) => ( +
  1. + ))} +
+
+ + diff --git a/src/content/docs/docs/is-stackyard-for-you.mdx b/src/content/docs/docs/is-stackyard-for-you.mdx new file mode 100644 index 0000000..0b51eec --- /dev/null +++ b/src/content/docs/docs/is-stackyard-for-you.mdx @@ -0,0 +1,82 @@ +--- +title: Stackyard vs gethomepage, Dashy and Homarr +description: How Stackyard compares to gethomepage, Dashy and Homarr on setup, integrations and dependencies, and who each one suits. +--- + +import CompareTable from '../../../components/CompareTable.astro'; +import CalmContrast from '../../../components/CalmContrast.astro'; + +Self-hosted dashboards make different bets. Here is the one Stackyard makes, next to the three you are most likely to be choosing between. + +Badges.', + 'Counted from each project\u2019s own documentation and source, September 2026. Services are self-hosted ones with a built-in integration. Dependencies are the external runtime packages each project lists.', + 'No user management. The dashboard is password protected. For more than one dashboard, run a container each with its own password and move between them with the Dashboard switch widget.', + ]} + rows={[ + { + label: 'Settings', + values: ['Web UI', 'YAML file', 'YAML or UI', 'Web UI'], + }, + { + label: 'Custom API', + cellNote: 1, + values: ['Live badge', 'YAML widget', 'YAML widget', 'Admin widget'], + }, + { + label: 'Health checks', + values: ['HTTP, Docker', 'HTTP, ICMP, Docker', 'HTTP, ICMP', 'HTTP'], + }, + { + label: 'Self-hosted integrations', + labelNote: 2, + values: ['24', '134', '19', '40'], + }, + { + label: 'User accounts', + cellNote: 3, + values: ['None', 'None', 'Yes, with roles', 'Yes, with groups'], + }, + { + label: 'Runtime dependencies', + labelNote: 2, + values: ['None', '32', '34', '58'], + }, + ]} +/> + + + +A health badge is hidden while a service is fine, so a working Stackyard shows no badges at all. See [Badges](/docs/badges/). + +## Is Stackyard for you? + +### Stackyard is a good fit if + +- You want a launcher first. The dashboard's job is to get you into your apps, and to tell you when one is unhealthy. +- You configure things in a web UI. There is no YAML file to hand-edit. +- You look at the dashboard many times a day. It is designed to stay calm at that frequency rather than to impress once. +- You want values from your own services on the tiles, without waiting for someone to write a widget for them. +- You want it to install to a phone home screen and work in six languages, right to left included. +- Accessibility matters to you. Stackyard is built to WCAG 2.2 level AA. See [Accessibility](/docs/accessibility/). + +### Another dashboard is a better fit if + +- You want a metrics dashboard. Stackyard shows current values, not history, and it does not alert. It sits next to a monitoring stack rather than replacing one. +- You want configuration as code. State lives in a single JSON file that the web UI writes. You can [export and import](/docs/import-export/backup-and-restore/) it, but it is not designed to be hand-edited or templated in a Git repository. +- You need user accounts. There is one admin password and one shared dashboard per instance. For a separate dashboard per person, room or site, run separate instances and link them with the [Dashboard switch](/docs/widgets/dashboard-switch/) widget. See [Security](/docs/security/). +- You want a plugin ecosystem. The widget set is fixed, plus a [Custom](/docs/widgets/custom/) widget that embeds any web page. + +## Where to go next + +Stackyard imports links and folders from gethomepage and Dashy configuration +files, so moving over is a file upload. See +[Migrating from another dashboard](/docs/import-export/migrating/). + +Look around the [demo](https://demo.sandobserver.com). Give the first load a +moment. + +Install it with [Docker](/docs/installation/docker/), then work through +[First setup](/docs/first-setup/). diff --git a/src/styles/docs.css b/src/styles/docs.css index 9c1e223..79a960f 100644 --- a/src/styles/docs.css +++ b/src/styles/docs.css @@ -665,7 +665,11 @@ button.sy-iconbtn:focus-visible { .sl-markdown-content :where( .sy-tiles, .sy-tile, .sy-order, .sy-order__step, .sy-group, .sy-row, .sy-shot, .sy-tile__art, .sy-row__label, .sy-row__pills, .sy-row__acts, - .wpv, .wpv__mat, .wpv__card, .wpv-pair + .wpv, .wpv__mat, .wpv__card, .wpv-pair, + .sy-cmp, .sy-cmp__scroll, .sy-cmp__notes, + .sy-cc, .sy-cc__pair, .sy-cc__panel, .sy-cc__screen, .sy-cc__row, .sy-cc__head, + .sy-cc__stats, .sy-cc__stat, .sy-cc__grid, .sy-cc__tile, .sy-cc__card, + .sy-cc__head, .sy-cc__names ) > * { margin-top: 0; }