diff --git a/index.html b/index.html index 700f754..033840d 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ syco.me — Dashboard + diff --git a/package-lock.json b/package-lock.json index 0536f75..95acd07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "express": "^4.19.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.24.0", "socket.io-client": "^4.8.3" }, "devDependencies": { @@ -813,6 +814,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -912,9 +922,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -929,9 +936,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -946,9 +950,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -963,9 +964,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -980,9 +978,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -997,9 +992,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1014,9 +1006,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1031,9 +1020,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1048,9 +1034,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1065,9 +1048,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1082,9 +1062,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1099,9 +1076,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1116,9 +1090,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2800,6 +2771,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index d47c08e..7b8f966 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "express": "^4.19.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.24.0", "socket.io-client": "^4.8.3" }, "devDependencies": { diff --git a/public/icons/Fritz!_Logo.svg.png b/public/icons/Fritz!_Logo.svg.png new file mode 100644 index 0000000..b74cfcd Binary files /dev/null and b/public/icons/Fritz!_Logo.svg.png differ diff --git a/public/icons/adguard-home.svg b/public/icons/adguard-home.svg new file mode 100644 index 0000000..e3e837d --- /dev/null +++ b/public/icons/adguard-home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/authentik.svg b/public/icons/authentik.svg new file mode 100644 index 0000000..7cbe6ea --- /dev/null +++ b/public/icons/authentik.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/bazarr.svg b/public/icons/bazarr.svg new file mode 100644 index 0000000..3cc113e --- /dev/null +++ b/public/icons/bazarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/copyparty.svg b/public/icons/copyparty.svg new file mode 100644 index 0000000..6789210 --- /dev/null +++ b/public/icons/copyparty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/crowdsec.svg b/public/icons/crowdsec.svg new file mode 100644 index 0000000..d0c9288 --- /dev/null +++ b/public/icons/crowdsec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dbgate.png b/public/icons/dbgate.png new file mode 100644 index 0000000..1fd6260 Binary files /dev/null and b/public/icons/dbgate.png differ diff --git a/public/icons/docsight-light.svg b/public/icons/docsight-light.svg new file mode 100644 index 0000000..10c2694 --- /dev/null +++ b/public/icons/docsight-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/favicon.png b/public/icons/favicon.png new file mode 100644 index 0000000..2cbbe89 Binary files /dev/null and b/public/icons/favicon.png differ diff --git a/public/icons/firefly-iii.svg b/public/icons/firefly-iii.svg new file mode 100644 index 0000000..c8b0d2f --- /dev/null +++ b/public/icons/firefly-iii.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/gitea.svg b/public/icons/gitea.svg new file mode 100644 index 0000000..d9eb11a --- /dev/null +++ b/public/icons/gitea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/grafana.svg b/public/icons/grafana.svg new file mode 100644 index 0000000..c482be9 --- /dev/null +++ b/public/icons/grafana.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/headscale.svg b/public/icons/headscale.svg new file mode 100644 index 0000000..316fc34 --- /dev/null +++ b/public/icons/headscale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/it-tools.svg b/public/icons/it-tools.svg new file mode 100644 index 0000000..8395b08 --- /dev/null +++ b/public/icons/it-tools.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/jellyfin.svg b/public/icons/jellyfin.svg new file mode 100644 index 0000000..f1703bb --- /dev/null +++ b/public/icons/jellyfin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lidarr.svg b/public/icons/lidarr.svg new file mode 100644 index 0000000..f25f408 --- /dev/null +++ b/public/icons/lidarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/linux-update-dashboard.svg b/public/icons/linux-update-dashboard.svg new file mode 100644 index 0000000..97523ce --- /dev/null +++ b/public/icons/linux-update-dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/navidrome.svg b/public/icons/navidrome.svg new file mode 100644 index 0000000..5d567f7 --- /dev/null +++ b/public/icons/navidrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/overseerr.svg b/public/icons/overseerr.svg new file mode 100644 index 0000000..d586c23 --- /dev/null +++ b/public/icons/overseerr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/paperless-ngx.svg b/public/icons/paperless-ngx.svg new file mode 100644 index 0000000..c0b7489 --- /dev/null +++ b/public/icons/paperless-ngx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/portainer.svg b/public/icons/portainer.svg new file mode 100644 index 0000000..ab03ab6 --- /dev/null +++ b/public/icons/portainer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/prowlarr.svg b/public/icons/prowlarr.svg new file mode 100644 index 0000000..b0adcbc --- /dev/null +++ b/public/icons/prowlarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/proxmox.svg b/public/icons/proxmox.svg new file mode 100644 index 0000000..b5fd75f --- /dev/null +++ b/public/icons/proxmox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/qbittorrent.svg b/public/icons/qbittorrent.svg new file mode 100644 index 0000000..9a8dd44 --- /dev/null +++ b/public/icons/qbittorrent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/radarr.svg b/public/icons/radarr.svg new file mode 100644 index 0000000..a84ce5d --- /dev/null +++ b/public/icons/radarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/romm.svg b/public/icons/romm.svg new file mode 100644 index 0000000..d50c6e1 --- /dev/null +++ b/public/icons/romm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shoko-server.svg b/public/icons/shoko-server.svg new file mode 100644 index 0000000..344b9c2 --- /dev/null +++ b/public/icons/shoko-server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/sonarr.svg b/public/icons/sonarr.svg new file mode 100644 index 0000000..54108f4 --- /dev/null +++ b/public/icons/sonarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/synology.svg b/public/icons/synology.svg new file mode 100644 index 0000000..67b1445 --- /dev/null +++ b/public/icons/synology.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/termix.svg b/public/icons/termix.svg new file mode 100644 index 0000000..de62252 --- /dev/null +++ b/public/icons/termix.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/uptime-kuma.svg b/public/icons/uptime-kuma.svg new file mode 100644 index 0000000..5f8909b --- /dev/null +++ b/public/icons/uptime-kuma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/vaultwarden.svg b/public/icons/vaultwarden.svg new file mode 100644 index 0000000..f3eb10c --- /dev/null +++ b/public/icons/vaultwarden.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/woodpecker-ci.svg b/public/icons/woodpecker-ci.svg new file mode 100644 index 0000000..6a816f4 --- /dev/null +++ b/public/icons/woodpecker-ci.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/yugiohCard.jpg b/public/icons/yugiohCard.jpg new file mode 100644 index 0000000..47c956c Binary files /dev/null and b/public/icons/yugiohCard.jpg differ diff --git a/src/App.tsx b/src/App.tsx index ed4a29d..746bd4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom' import { Header } from './components/Header' import { ProxmoxWidget } from './components/widgets/ProxmoxWidget' import { NasWidget } from './components/widgets/NasWidget' @@ -17,49 +18,60 @@ import { LokiWidget } from './components/widgets/LokiWidget' import { JellyfinWidget } from './components/widgets/JellyfinWidget' import { NavidromeWidget } from './components/widgets/NavidromeWidget' import { RommWidget } from './components/widgets/RommWidget' +import { AppsPage } from './pages/AppsPage' -export default function App() { +function DashboardPage() { return ( <> -
-
-
-
+
Infrastructure
+
+ + + + + +
-
Infrastructure
-
- - - - - -
+
Media
+
+ + + + + + +
-
Media
-
- - - - - - -
+
Monitoring
+
+ + + + + +
-
Monitoring
-
- - - - - -
- -
Access
-
- - -
+
Access
+
+ +
) } + +export default function App() { + return ( + +
+
+
+
+ + } /> + } /> + +
+ + ) +} diff --git a/src/components/AppsSection.tsx b/src/components/AppsSection.tsx new file mode 100644 index 0000000..6880705 --- /dev/null +++ b/src/components/AppsSection.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react' +import { appGroups } from '../config/apps' + +function AppTile({ name, url, icon }: { name: string; url: string; icon: string }) { + const [imgError, setImgError] = useState(false) + + return ( + + {!imgError ? ( + {name} setImgError(true)} + /> + ) : ( +
+ {name.charAt(0).toUpperCase()} +
+ )} + {name} +
+ ) +} + +export function AppsSection() { + return ( + <> + {appGroups.map(group => ( +
+
{group.name}
+
+ {group.apps.map(app => ( + + ))} +
+
+ ))} + + ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 73670ce..95b991c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { NavLink } from 'react-router-dom' export function Header() { const [time, setTime] = useState('') @@ -18,13 +19,17 @@ export function Header() { return (
-
S
+ logo
syco.me
homelab dashboard
+
{time}
{date}
diff --git a/src/config/apps.ts b/src/config/apps.ts new file mode 100644 index 0000000..f8b9520 --- /dev/null +++ b/src/config/apps.ts @@ -0,0 +1,72 @@ +const ICONS = '/icons' + +export interface AppLink { + name: string + url: string + icon: string +} + +export interface AppGroup { + name: string + apps: AppLink[] +} + +export const appGroups: AppGroup[] = [ + { + name: 'Infrastructure', + apps: [ + { name: 'Proxmox', url: 'https://proxmox.syco.me', icon: `${ICONS}/proxmox.svg` }, + { name: 'Synology NAS', url: 'https://nas.syco.me', icon: `${ICONS}/synology.svg` }, + { name: 'AdGuard Home', url: 'https://adguard.syco.me', icon: `${ICONS}/adguard-home.svg` }, + { name: 'Headscale', url: 'https://headscale.syco.me/admin', icon: `${ICONS}/headscale.svg` }, + { name: 'FritzBox', url: 'http://fritz.box', icon: `${ICONS}/Fritz!_Logo.svg.png` }, + { name: 'Portainer', url: 'https://portainer.syco.me', icon: `${ICONS}/portainer.svg` }, + { name: 'CrowdSec', url: 'https://crowdsec.syco.me', icon: `${ICONS}/crowdsec.svg` }, + ], + }, + { + name: 'Auth & Access', + apps: [ + { name: 'Authentik', url: 'https://auth.syco.me', icon: `${ICONS}/authentik.svg` }, + { name: 'Vaultwarden', url: 'https://vaultwarden.syco.me', icon: `${ICONS}/vaultwarden.svg` }, + { name: 'Termix', url: 'https://termix.syco.me', icon: `${ICONS}/termix.svg` }, + ], + }, + { + name: 'Media', + apps: [ + { name: 'Jellyfin', url: 'https://jellyfin.syco.me', icon: `${ICONS}/jellyfin.svg` }, + { name: 'Navidrome', url: 'https://music.syco.me', icon: `${ICONS}/navidrome.svg` }, + { name: 'Overseerr', url: 'https://overseerr.syco.me', icon: `${ICONS}/overseerr.svg` }, + { name: 'Radarr', url: 'https://radarr.syco.me', icon: `${ICONS}/radarr.svg` }, + { name: 'Sonarr', url: 'https://sonarr.syco.me', icon: `${ICONS}/sonarr.svg` }, + { name: 'Lidarr', url: 'https://lidarr.syco.me', icon: `${ICONS}/lidarr.svg` }, + { name: 'Prowlarr', url: 'https://prowlarr.syco.me', icon: `${ICONS}/prowlarr.svg` }, + { name: 'Bazarr', url: 'https://bazarr.syco.me', icon: `${ICONS}/bazarr.svg` }, + { name: 'qBittorrent', url: 'https://bittorrent.syco.me', icon: `${ICONS}/qbittorrent.svg` }, + { name: 'RomM', url: 'https://romm.syco.me', icon: `${ICONS}/romm.svg` }, + { name: 'Shoko', url: 'http://shoko.internal', icon: `${ICONS}/shoko-server.svg` }, + ], + }, + { + name: 'Monitoring', + apps: [ + { name: 'Grafana', url: 'https://grafana.syco.me', icon: `${ICONS}/grafana.svg` }, + { name: 'Uptime Kuma', url: 'https://uptime.syco.me', icon: `${ICONS}/uptime-kuma.svg` }, + { name: 'Update Dashboard', url: 'https://update-dashboard.syco.me', icon: `${ICONS}/linux-update-dashboard.svg` }, + ], + }, + { + name: 'Dev & Tools', + apps: [ + { name: 'Gitea', url: 'https://git.syco.me', icon: `${ICONS}/gitea.svg` }, + { name: 'Woodpecker', url: 'https://woodpecker.syco.me', icon: `${ICONS}/woodpecker-ci.svg` }, + { name: 'DbGate', url: 'https://dbgate.syco.me', icon: `${ICONS}/dbgate.png` }, + { name: 'IT-Tools', url: 'https://it-tools.syco.me', icon: `${ICONS}/it-tools.svg` }, + { name: 'Docsight', url: 'http://docsight.internal', icon: `${ICONS}/docsight-light.svg` }, + { name: 'Firefly III', url: 'https://firefly.syco.me', icon: `${ICONS}/firefly-iii.svg` }, + { name: 'Copyparty', url: 'https://copyparty.syco.me', icon: `${ICONS}/copyparty.svg` }, + { name: 'Yu-Gi-Oh', url: 'https://yugioh.syco.me', icon: `${ICONS}/yugiohCard.jpg` }, + ], + }, +] diff --git a/src/index.css b/src/index.css index 416d3eb..c5194c1 100644 --- a/src/index.css +++ b/src/index.css @@ -63,10 +63,8 @@ header { .logo { display: flex; align-items: center; gap: 10px; } .logo-mark { width: 32px; height: 32px; - background: linear-gradient(135deg, var(--accent), var(--accent2)); border-radius: 8px; - display: flex; align-items: center; justify-content: center; - font-size: 14px; font-weight: 800; letter-spacing: -1px; color: #fff; + object-fit: contain; } .logo-text { font-size: 18px; font-weight: 700; letter-spacing: -0.5px; } .logo-sub { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted); } @@ -226,6 +224,94 @@ header { .topbar-right { gap: 12px; } } +/* ── Nav tabs ── */ +.nav-tabs { + display: flex; + gap: 4px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 3px; +} +.nav-tab { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 500; + padding: 5px 14px; + border-radius: 6px; + text-decoration: none; + color: var(--text2); + transition: background 0.15s, color 0.15s; +} +.nav-tab:hover { color: var(--text); } +.nav-tab-active { background: var(--surface2); color: var(--text); } + +/* ── Apps Section ── */ +.app-group { margin-bottom: 28px; } + +.app-group-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; font-weight: 500; + letter-spacing: 2px; text-transform: uppercase; + color: var(--muted); + margin-bottom: 12px; +} + +.app-grid { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.app-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 14px 12px; + width: 90px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + text-decoration: none; + color: var(--text2); + font-size: 11px; + text-align: center; + transition: border-color 0.2s, transform 0.2s, color 0.2s; + cursor: pointer; +} +.app-tile:hover { + border-color: var(--accent); + transform: translateY(-2px); + color: var(--text); +} + +.app-icon { + width: 36px; + height: 36px; + object-fit: contain; +} + +.app-icon-fallback { + width: 36px; + height: 36px; + border-radius: 8px; + background: var(--surface2); + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + color: var(--accent); +} + +.app-name { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: inherit; + line-height: 1.3; + word-break: break-word; +} + /* ── Scrollbar ── */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } diff --git a/src/pages/AppsPage.tsx b/src/pages/AppsPage.tsx new file mode 100644 index 0000000..13fd3ed --- /dev/null +++ b/src/pages/AppsPage.tsx @@ -0,0 +1,10 @@ +import { AppsSection } from '../components/AppsSection' + +export function AppsPage() { + return ( +
+
Apps
+ +
+ ) +}