Compare commits

93 Commits

Author SHA1 Message Date
Kroonk
3be11fb055 Use siteMonitor instead of ping for HTTP port-level status checks and change Jellyfin to use local IP
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-07-02 11:36:47 +02:00
Kroonk
88761708da Integrate VM 192.168.178.111 services into public and admin dashboards using ping monitoring
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 14s
2026-07-02 11:31:52 +02:00
Kroonk
7227550998 Exclude mischlabs_ssh_key from git tracking
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 15s
2026-07-02 11:29:07 +02:00
Kroonk
3f3b8c6003 Deploy Filebrowser config editor container and add integration under Arbeit category on Admin dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 16s
2026-07-01 18:11:42 +02:00
Kroonk
936d1d7b17 Configure 85% opacity dark slate overlay for background image to make it very subtle
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 15s
2026-07-01 18:07:08 +02:00
Kroonk
3ba3638e5e Fix background visibility by using settings.yaml background property and making Next.js wrappers transparent in custom.css
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 14s
2026-07-01 18:04:15 +02:00
Kroonk
cc52ab9145 Add homepage documentation to docs folder
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 16s
2026-07-01 17:59:59 +02:00
Kroonk
a43717f8ce Update Brain.md documentation with custom styling details
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 14s
2026-07-01 17:46:59 +02:00
Kroonk
b2f49b7d10 Add glassmorphic custom.css and custom background image for public and admin instances
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 9s
2026-07-01 17:45:19 +02:00
Kroonk
1cb1661374 Update Brain.md documentation to describe public/admin dashboard split
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 1m2s
2026-07-01 17:37:21 +02:00
Kroonk
285280fa01 Split dashboard into public and admin instances with separate config-public and config-admin directories
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Has been cancelled
2026-07-01 17:37:08 +02:00
Kroonk
6f54867bc3 Update Brain.md documentation with background image removal details
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 56s
2026-07-01 17:31:07 +02:00
Kroonk
edd59e73e0 Remove background image to use default dark theme
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Has been cancelled
2026-07-01 17:30:54 +02:00
Kroonk
00fe37374f Update Brain.md with Host validation hotfix details
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 1m1s
2026-07-01 17:25:56 +02:00
Kroonk
16176575b3 Add HOMEPAGE_ALLOWED_HOSTS to docker-compose.yml to fix Host validation failed error
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Has been cancelled
2026-07-01 17:25:47 +02:00
Kroonk
4cddac67dd Update Brain.md with Homepage migration details
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 57s
2026-07-01 17:23:55 +02:00
Kroonk
5aea9e5403 Replace custom dashboard with gethomepage.dev and configure services, docker, settings, and widgets
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Has been cancelled
2026-07-01 17:23:26 +02:00
Kroonk
687a6a947b feat: serve matrix .well-known endpoints directly from Node server.js
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-22 12:31:55 +02:00
Kroonk
fb3c7a99f1 feat: Service-Editor mit vollem CRUD und Server-Speichern
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 16s
2026-05-22 00:43:25 +02:00
Kroonk
072fc9b9f9 Integrate dashboard hamburger menu inside search box and swap control-bar layout
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-22 00:32:12 +02:00
Kroonk
47d73a466b Fix CSS hidden specificity and add premium header user badge
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 14s
2026-05-22 00:29:03 +02:00
Kroonk
205cb1d948 feat: implement automatic SSO login redirect on admin panel, with logout protection
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-22 00:26:27 +02:00
Kroonk
8ebee64543 fix: bypass service worker cache for dynamic /api/ endpoints, resolving stale favorites cache
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-22 00:23:37 +02:00
Kroonk
b0bcc8abbd feat: replace online status metric with favorites counter, ordered Dienste first
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-22 00:20:59 +02:00
Kroonk
57b39f8771 content: change subtitle to Die Selfhost-Werkstatt
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-22 00:18:36 +02:00
Kroonk
6cea1d9de9 fix: remove dark control-bar background to fix black bar visual glitch
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-22 00:14:02 +02:00
Kroonk
0a72e63e84 style: remove PWA metric card and optimize category filters for mobile usability
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-22 00:10:41 +02:00
Kroonk
1cdd944aca fix: only render admin panel link for admins
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-22 00:04:56 +02:00
Kroonk
df0a5d5035 fix: move dashboard actions into compact menu
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-22 00:02:51 +02:00
Kroonk
ae775c4dcf feat: share SSO session with admin panel
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 23:55:18 +02:00
Kroonk
14505c2296 fix: polish dashboard view toggle styling
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 23:52:15 +02:00
Kroonk
9572b6116a feat: add dashboard view toggle
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 23:48:58 +02:00
Kroonk
4c7ef500a1 fix: limit mobile handoff delay to Nextcloud
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 23:20:11 +02:00
Kroonk
d40329ae7c feat: add SSO user favorites dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 23:18:23 +02:00
Kroonk
fb540813ff fix: avoid web fallback after app handoff
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 23:05:09 +02:00
Kroonk
705374cacd fix: use current logo for all favicons
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 14s
2026-05-21 22:59:55 +02:00
Kroonk
cde4035eb3 fix: use Nextcloud iOS deep link scheme
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 22:55:17 +02:00
Kroonk
4bb07122ac fix: avoid invalid Nextcloud browser login flow
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 22:48:02 +02:00
Kroonk
718d843df2 feat: open Nextcloud login flow on mobile
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 22:43:52 +02:00
Kroonk
60ed7d624e feat: automate Keycloak session settings config on dashboard startup
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 22:31:04 +02:00
Kroonk
0d4377aed2 feat: implement automatic smart mobile app launcher with custom schemes and web fallbacks
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 22:28:22 +02:00
Kroonk
e663534000 docs: document recent dashboard improvements and add global AI prompt guideline to Brain.md
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 10s
2026-05-21 22:08:38 +02:00
Kroonk
c79f856e31 style: apply premium blue-to-purple gradient to brand text logo
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 20:44:14 +02:00
Kroonk
0a044bbb92 feat: implement container logs modal, service diagnostics inspector, and directory sizer tool
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 18:49:59 +02:00
Kroonk
6ff401ad74 feat: add consolidated Übersicht (Overview) dashboard tab with real-time mini-cards
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 18:41:11 +02:00
Kroonk
32bed2d65e fix: bump css and js version parameters to force cache busting
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 18:36:21 +02:00
Kroonk
223f5abf78 feat: add hamburger menu navigation drawer and tabbed views for admin sections
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 18:32:30 +02:00
Kroonk
28bf2c5dfe style: wrap service editor in collapsible card and make it narrower
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 18:26:40 +02:00
Kroonk
b3c1fbc9bd feat: fix watchtower manual run stuck states in both client and server
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 18:25:27 +02:00
Kroonk
76b7a96673 feat: make ops cards collapsible, hide notice and user display, increase button sizes
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 18:21:47 +02:00
Kroonk
995a397b2e feat: turn status items into mini clickable cards with pill-shaped status LEDs
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 18:13:21 +02:00
Kroonk
4cb0e89e92 style: minimalist design for services status board with tooltip and horizontal traffic-light bars
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 18:09:31 +02:00
Kroonk
e603bdc3be feat: Add premium server-side Dienste-Status (Realtime) Board to admin panel
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 18:04:53 +02:00
Kroonk
d4f6e56ff0 fix: Bump CSS and JS version to clear browser and service worker cache
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 17:57:27 +02:00
Kroonk
ddca0ae831 fix: Parse both JSON and text formatted logs in parseWatchtower
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 17:53:41 +02:00
Kroonk
ae116f52f9 style: Remove card hover peek raccoon
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 17:48:53 +02:00
Kroonk
8946cbecf6 feat: Add peeking Bongo Cat Easter Egg with micro-animations
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 17:41:28 +02:00
Kroonk
e9e589ddcc Auto-refresh Watchtower status
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 17:30:23 +02:00
Kroonk
2af7e65d4a Simplify dashboard service layout
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 17:27:31 +02:00
Kroonk
fddb2e3544 Add temporary danger restart mode
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 17:21:52 +02:00
Kroonk
d62224366b Fix admin ops status refresh
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 17:16:58 +02:00
Kroonk
5b252f17fd Clean up manual Watchtower containers
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 17:07:28 +02:00
Kroonk
c3ac1be083 Show manual Watchtower run status
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 16:49:41 +02:00
Kroonk
e98130d274 Add manual Watchtower trigger
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-21 16:43:59 +02:00
Kroonk
10ad368214 Add admin container restart controls
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 16:40:52 +02:00
Kroonk
ea52f223cd Improve ops status diagnostics
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 16:26:38 +02:00
Kroonk
29b3b623eb Add NAS status proxy
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 33s
2026-05-21 16:22:34 +02:00
Kroonk
f0674cda37 Update dashboard service categories
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 10s
2026-05-21 16:04:31 +02:00
Kroonk
8f2b4faad9 Handle browser-blocked status checks
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
2026-05-21 15:40:50 +02:00
Kroonk
2125601e56 Add SSO admin console for dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 15:25:51 +02:00
Kroonk
470220b8f4 Rework MischLabs into service dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 15:11:52 +02:00
Kroonk
0c3838da8a Bump iOS PWA icon filename
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 10s
2026-05-21 14:35:23 +02:00
Kroonk
325c30414c Use MischLabs artwork for PWA icons
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 14:07:19 +02:00
Kroonk
1ecbcc38ae Add PWA support for MischLabs
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
2026-05-21 13:56:14 +02:00
Kroonk
81ddb781ca Improve landing page mobile performance
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 9s
2026-05-20 18:24:11 +02:00
Kroonk
75b8a66c62 Refine cat hover expression
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 10s
2026-05-20 18:20:52 +02:00
Kroonk
6bb915299f Place cat peek behind cards
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 9s
2026-05-20 18:15:39 +02:00
Kroonk
271612af32 Add cat peek hover to service cards
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 9s
2026-05-20 18:09:42 +02:00
Kroonk
c0680700a3 Optimize: Migrate mischlabs fully to Gitea Local Package Registry
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
2026-05-20 16:43:54 +02:00
Kroonk
bf404e34c3 Update: Set mischlabs image to GHCR repository in docker-compose
All checks were successful
Build & Push Docker Image to GHCR / build-and-push (push) Successful in 14s
2026-05-20 16:37:21 +02:00
Kroonk
c132cd372d Optimize: Remove Buildx to fix 5-minute hang and use native docker build
All checks were successful
Build & Push Docker Image to GHCR / build-and-push (push) Successful in 21s
2026-05-20 16:31:39 +02:00
Kroonk
036196abe1 Fix: Add Set up Docker Buildx step
Some checks failed
Build & Push Docker Image to GHCR / build-and-push (push) Failing after 5m10s
2026-05-20 16:25:59 +02:00
Kroonk
22411b4cc3 Fix: Explicitly use Bourne shell (sh) for Install Docker CLI step
Some checks failed
Build & Push Docker Image to GHCR / build-and-push (push) Failing after 13s
2026-05-20 16:25:02 +02:00
Kroonk
13e1f35ae0 Fix: Add robust Docker CLI installation to Gitea workflow using static binaries
Some checks failed
Build & Push Docker Image to GHCR / build-and-push (push) Failing after 5s
2026-05-20 16:23:48 +02:00
Kroonk
7ae5007fb5 Add Gitea Actions workflow to build and push docker image
Some checks failed
Build & Push Docker Image to GHCR / build-and-push (push) Failing after 10s
2026-05-20 16:20:44 +02:00
Kroonk
50d162af57 Docs-Kachel zu Books.mischlabs.de geaendert 2026-05-20 15:40:52 +02:00
Kroonk
9254e9d631 Fix SSO banner: force horizontal layout with smaller elements
Use higher specificity selectors to override base card styles.
Smaller icon, font sizes, and tighter padding for compact banner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:46:47 +02:00
Kroonk
6d9dca95f0 Add cache-busting version to CSS link
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:44:49 +02:00
Kroonk
e04632a3e1 Reduce auth banner height and normalize spacing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:36:35 +02:00
Kroonk
0f35698eb8 Add auth.mischlabs.de as wide banner card above service grid
Horizontal banner layout with shield icon, spanning full grid width.
Responsive: collapses to vertical on mobile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:34:43 +02:00
Kroonk
28f5654060 Remove FoodLabs/Mealie tile (was only a test service)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:33:32 +02:00
Kroonk
08d5da969d Update Brain.md with MiChess, auth project reference, and learnings
Added MiChess as 9th service, documented auth/Keycloak sister project,
and captured DNS/git insights from deployment session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 20:40:59 +02:00
Kroonk
7ec77e4ee2 Add MiChess service tile to landing page
New tile for michess.mischlabs.de (chess) with chess piece icon
and staggered animation delay for 9th card.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 20:36:16 +02:00
50 changed files with 7604 additions and 360 deletions

59
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Build & Push Docker Image to Gitea Registry
on:
push:
branches: [master]
env:
REGISTRY: git.mischlabs.de
REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/mischlabs
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
shell: sh
run: |
if ! command -v docker >/dev/null 2>&1; then
echo "Docker CLI not found. Installing static binary..."
if command -v curl >/dev/null 2>&1; then
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-26.1.4.tgz | tar -xz -C /usr/local/bin --strip-components=1 docker/docker
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-26.1.4.tgz | tar -xz -C /usr/local/bin --strip-components=1 docker/docker
else
echo "Neither curl nor wget found! Attempting package manager install..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y docker.io
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache docker-cli
else
echo "Cannot install Docker CLI!"
exit 1
fi
fi
else
echo "Docker CLI already installed."
fi
docker --version
- name: Log in to Gitea Package Registry
shell: sh
run: |
if [ -z "${{ secrets.REGISTRY_USER }}" ] || [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "Missing registry credentials."
echo "Create repository secrets REGISTRY_USER and REGISTRY_TOKEN."
echo "REGISTRY_TOKEN must be a Gitea access token with package write permission."
exit 1
fi
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and push Docker image
shell: sh
run: |
docker build -t ${{ env.REGISTRY_IMAGE }}:latest -t ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} .
docker push ${{ env.REGISTRY_IMAGE }}:latest
docker push ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@ Thumbs.db
*~
.vscode/
.claude/
filebrowser.db
filebrowser.db-journal
mischlabs_ssh_key

175
Brain.md
View File

@@ -3,47 +3,70 @@
## Projektziel
Eine ansprechende Landing Page fuer mischlabs.de, die alle Dienste als Kacheln mit direkten Verlinkungen anzeigt. Laeuft als Docker-Container und wird ueber Git auto-deployed.
Aktueller Stand: Die Seite ist keine reine Linkliste mehr, sondern ein kompaktes Service-Dashboard mit Kategorien, Suche, Hintergrund-Statuscheck und PWA-Unterstuetzung.
## Status
- [x] Git-Repo initialisiert und Remote verbunden (https://git.mischlabs.de/MrDiderot/mischlabs.git)
- [x] Dienste-Liste vom Screenshot erfasst (8 Services)
- [x] Dienste-Liste vom Screenshot erfasst (9 Services inkl. MiChess)
- [x] Landing Page Design und Implementierung (index.html + style.css)
- [x] Docker-Setup (Dockerfile, docker-compose.yml, nginx.conf)
- [ ] Erster Push zum Remote
- [x] PWA-Grundlage fuer iOS/Home-Screen-Installation
- [x] Dashboard v2 mit Kategorien, Suche und Live-Statusindikatoren
- [x] Push zum Remote
- [x] MiChess-Kachel hinzugefuegt (Commit lokal, Push steht aus wg. Auth-Fehler)
- [ ] Push des MiChess-Commits (Git-Auth-Token erneuern)
## Verwandte Projekte
- **auth** (d:\Vibecoding\Website\auth) - Keycloak SSO fuer alle Dienste
- Repo: https://git.mischlabs.de/MrDiderot/auth.git
- Subdomain: auth.mischlabs.de
- Siehe auth/Brain.md fuer Details
## Dienste-Uebersicht (8 Stueck)
| # | Subdomain | Beschreibung | Farbe/Accent |
|---|-----------|-------------|--------------|
| 1 | audiobook.mischlabs.de | Hoerbuecher | #22c55e (Gruen) |
| 2 | foodlabs.mischlabs.de | Rezepte | #f97316 (Orange) |
| 1 | drive.mischlabs.de | Cloud-Speicher | #4285f4 (Blau) |
| 2 | git.mischlabs.de | Code-Repositories | #f05033 (Rot) |
| 3 | movies.mischlabs.de | Filme & Serien | #a855f7 (Lila) |
| 4 | drive.mischlabs.de | Cloud-Speicher | #4285f4 (Blau) |
| 4 | audiobook.mischlabs.de | Hoerbuecher | #22c55e (Gruen) |
| 5 | password.mischlabs.de | Passwort-Manager | #eab308 (Gelb) |
| 6 | tom.mischlabs.de | Persoenliche Seite | #ec4899 (Pink) |
| 7 | git.mischlabs.de | Code-Repositories | #f05033 (Rot) |
| 8 | docs.mischlabs.de | Dokumentation | #06b6d4 (Cyan) |
| 6 | docs.mischlabs.de | Dokumentation | #06b6d4 (Cyan) |
| 7 | michess.mischlabs.de | Schach | #10b981 (Gruen) |
| 8 | tom.mischlabs.de | Persoenliche Seite | #ec4899 (Pink) |
## Technische Entscheidungen
- **Reines HTML + CSS** (kein Framework, kein JS) - maximale Performance, minimales Docker-Image
- **Reines HTML + CSS mit minimalem Dashboard-/PWA-JS** (kein Framework) - maximale Performance, minimales Docker-Image
- **nginx:alpine** als Base-Image - extrem leichtgewichtig (~5MB)
- **Google Fonts (Inter)** fuer moderne Typografie
- **Inline SVG Icons** - keine externen Dependencies
- **CSS Grid** mit `auto-fill` + `minmax(240px, 1fr)` fuer responsives Layout
- **Dark Theme** mit subtilen Glow-Effekten
- **Staggered Animations**: Kacheln faden nacheinander ein
- **Staggered Animations**: Kacheln faden nacheinander ein (9 Stueck)
- **PWA**: Manifest, Apple-Touch-Icon, App-Icons und Service Worker fuer installierbare iOS/Web-App
- **Dashboard**: Kategorien `Zugang`, `Medien`, `Arbeit`, `Persoenlich`, Suche und clientseitige no-cors Statuschecks
- **Auth-Link**: Zeigt auf die Keycloak Account Console (`/realms/mischlabs/account`), nicht mehr auf die Admin-Konsole
- **Adminseite**: `/admin.html` nutzt Keycloak Authorization Code + PKCE fuer Login und bietet Editor/Statusdiagnose fuer Dienste
## Docker-Setup
- **Container-Name**: `mischlabs`
- **Base-Image**: `nginx:alpine`
- **Port-Mapping**: `8085:80` (anpassbar in docker-compose.yml)
- **Restart-Policy**: `unless-stopped`
- **Auto-Update**: Container wird ueber Git-Repo-Aenderungen neu gebaut
- **nginx.conf**: Caching fuer Static Assets (7 Tage), Security-Headers
## Dateistruktur
```
mischlabs/
index.html - Haupt-HTML mit allen 8 Service-Kacheln
index.html - Haupt-HTML mit allen 9 Service-Kacheln
style.css - Komplettes Styling (Dark Theme, Grid, Animationen)
app.js - Suche, Filter, Statuschecks, Service-Worker-Registrierung
admin.html - SSO-geschuetzte Adminoberflaeche
admin.js - Keycloak PKCE Login, Editor, Statusdiagnose
services.json - Zentrale Service-/Kategorie-Konfiguration
offline.html - Offline-Fallback fuer installierte PWA
manifest.webmanifest - PWA-Metadaten fuer installierbare App
sw.js - Schlanker Service Worker mit App-Shell-Cache
icons/ - App-Icons fuer iOS/PWA
Dockerfile - nginx:alpine mit kopierten Static Files
docker-compose.yml - Container-Definition (Name: mischlabs, Port 8085)
nginx.conf - Server-Konfiguration, Caching, Security-Headers
@@ -55,11 +78,135 @@ mischlabs/
1. Code aendern und committen
2. Push zu git.mischlabs.de
3. Auf dem Server: `git pull && docker compose up -d --build`
(oder per Webhook/CI automatisiert)
## Erkenntnisse & Notizen
- Alle Dienste laufen auf 192.168.178.88 mit verschiedenen Ports
- Oeffentlich erreichbar ueber *.mischlabs.de Subdomains (Reverse Proxy)
- Oeffentlich erreichbar ueber *.mischlabs.de Subdomains (Cloudflare Tunnel)
- Port 8085 gewaehlt da gaengige Ports (80, 8080, 8090) bereits belegt
- Kein JavaScript noetig - pure CSS reicht fuer alle Effekte
- `color-mix()` CSS-Funktion fuer dynamische Icon-Hintergruende
- Git-Push-Fehler aufgetreten (Authentication failed) - Token muss erneuert werden
- DNS: Alte A-Records (185.181.104.242) von INWX wurden geloescht, nur noch Tunnel-Eintraege
- NAS hat kein git installiert - entweder Git Server Paket installieren oder wget nutzen
## PWA-Stand 2026-05-21
- MischLabs wurde als PWA vorbereitet.
- iOS-relevante Meta-Tags in `index.html`:
- `apple-mobile-web-app-capable`
- `apple-mobile-web-app-status-bar-style`
- `apple-mobile-web-app-title`
- `apple-touch-icon`
- Manifest:
- `name`: MischLabs
- `display`: standalone
- `theme_color`: #10b981
- `background_color`: #0a0a0f
- Icons:
- `icons/app-icon-source.png` (1024x1024, quadratischer Ausschnitt aus dem MischLabs-Bild)
- `icons/apple-touch-icon-v2.png` (180x180, frischer Dateiname gegen iOS-Icon-Cache)
- `icons/apple-touch-icon.png` (180x180, alte Kompatibilitaetskopie)
- `icons/icon-192.png` (192x192)
- `icons/icon-512.png` (512x512)
- `icons/maskable-512.png` (512x512, maskable)
- Service Worker:
- Cache-Name: `mischlabs-pwa-v4`
- Cached die App-Shell (`/`, `index.html`, `offline.html`, `style.css?v=10`, `app.js?v=2`, `services.json?v=1`, Manifest und Icons)
- Navigationen fallen offline auf `offline.html` zurueck
- Dockerfile kopiert Manifest, Service Worker und Icons in das Nginx-Image.
- `nginx.conf` liefert `manifest.webmanifest` mit `application/manifest+json` und `sw.js` mit harten No-Cache-Headern aus.
## Dashboard-v2-Stand 2026-05-21
- Neues Logo oben nutzt `icons/app-icon-source.png`.
- Die alte oeffentliche Auth-Kachel wurde auf die Keycloak Account Console umgebogen:
`https://auth.mischlabs.de/realms/mischlabs/account`
- Kategorien:
- Zugang: SSO, Drive, Passwords
- Medien: Movies, Audiobooks, Books
- Arbeit: Git, MiChess
- Persoenlich: Tom
- `app.js` prueft Dienste im Hintergrund per `fetch(..., { mode: "no-cors" })`. Das ist bewusst nur ein Erreichbarkeitsindikator, kein vollwertiges Monitoring.
## Adminseite Stand 2026-05-21
- URL: `https://mischlabs.de/admin.html`
- SSO-Client in Keycloak benoetigt:
- Client ID: `mischlabs-admin`
- Client type: OpenID Connect
- Client authentication: Off/Public Client
- Standard flow: On
- Valid redirect URI: `https://mischlabs.de/admin.html`
- Valid post logout redirect URI: `https://mischlabs.de/admin.html`
- Web origin: `https://mischlabs.de`
- Scopes: `openid email profile`
- Erlaubter Admin in der statischen UI:
- `preferred_username=mrdiderot` oder `email=mail.misch@pm.me`
- Aktuelle Grenze: Weil mischlabs noch rein statisch ueber Nginx laeuft, kann `/admin.html` globale Aenderungen noch nicht serverseitig speichern. Der Editor speichert lokal im Browser (`localStorage`) und kann eine neue `services.json` exportieren. Fuer echte Live-Aenderungen braucht der naechste Schritt ein kleines Backend oder einen Gitea-Commit-Workflow.
- Statusdiagnose nutzt Browser-Fetch mit `mode: "no-cors"`. Dadurch erkennt sie Erreichbarkeit/Timeouts, aber bei fremden Subdomains keine echten HTTP-Statuscodes.
- Wenn der Browser die Pruefung wegen CORS/CORB blockiert (`Failed to fetch`), wird der Dienst **nicht** mehr als offline markiert, sondern als "Nicht im Browser pruefbar". Das betraf z. B. Vaultwarden/`password.mischlabs.de`.
- Fuer echte Fehlerdetails braucht es ein kleines Backend bzw. einen Statusproxy, der serverseitig `HEAD`/`GET` prueft.
## Statusproxy Stand 2026-05-21
- `mischlabs` wurde von Nginx auf einen kleinen Node-Server (`server.js`) umgestellt.
- Der Node-Server serviert die statischen Dateien weiter und stellt `/api/status` bereit.
- `/api/status` liefert:
- Docker-Container via `/var/run/docker.sock`
- Watchtower-Logs und letzten Watchtower-Lauf
- Speicherplatz fuer `/volume1` und `/volume2`, im Container gemountet als `/host/volume1` und `/host/volume2`
- Serverseitige Service-Checks mit HTTP-Status und Latenz
- NAS-Compose fuer `mischlabs` braucht diese Mounts:
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /volume1:/host/volume1:ro
- /volume2:/host/volume2:ro
```
- Aktuelle NAS-Werte bei Planung:
- `/volume1`: 3.7T total, 2.5T used, 1.2T free, 68%
- `/volume2`: 104G total, 88G used, 12G free, 89%
- Watchtower: `Scanned=23`, `Updated=0`, `Failed=0` beim manuellen Lauf um 2026-05-21 14:16 UTC
## Globale KI-Systemeinstellung (WICHTIG!)
> [!IMPORTANT]
> **Arbeitsanweisung für alle KI-Systeme (LLM / Agenten):**
> Nach dem erfolgreichen Abschluss einer Aufgabe oder eines Meilensteins MUSS automatisch ein strukturierter Eintrag in dieser `Brain.md` vorgenommen werden. Dieser Eintrag soll dokumentieren:
> 1. Welche Änderungen an welchen Dateien vorgenommen wurden (inklusive Begründung).
> 2. Welche neuen Features, Endpoints oder Design-Anpassungen integriert wurden.
> 3. Der aktuelle Stand des Deployments und eventuelle offene Punkte oder nächste Schritte.
> Dies stellt sicher, dass alle nachfolgenden KI-Systeme und der Benutzer stets einen konsistenten, lückenlosen Projektverlauf nachvollziehen können.
## Letzte Änderungen & Meilensteine (Stand: 2026-05-21)
- **Erweitertes Admin-Tool (Universal Admin Panel):**
- **Docker Container Logs Modal:** Integration eines interaktiven, terminal-ähnlichen Log-Overlays in `admin.html` und `admin.js`. Jeder Container auf der Übersicht sowie Watchtower bieten nun einen **Logs**-Button. Logs können dynamisch mit Tiefenbeschränkungen (`50`, `150`, `300`, `500` Zeilen) abgefragt und manuell aktualisiert werden. Backend-Endpunkt: GET `/api/containers/:name/logs`.
- **Live Service-Inspektion (`🔍`):** Status-Kacheln der Dienste bieten ein Lupensymbol. Klicks darauf starten eine serverseitige Node-Anfrage (CORS-frei) an die Zieladresse und geben Antwortlatenz, HTTP-Statuscode, vollständige HTTP-Header und eine Vorschau der ersten 1000 Zeichen der Antwort zurück. Backend-Endpunkt: GET `/api/services/inspect`.
- **Interaktiver NAS Verzeichnis-Scanner:** Hinzufügen eines Ordnergrößen-Scanners im NAS-Speicher-Panel. Berechnet rekursiv Ordnergrößen für gemountete Volumes (`/host/volume1`, `/host/volume2`), sortiert sie absteigend und erlaubt durch Anklicken von Unterordnern ein direktes Weiterscannen im entsprechenden Pfad. Backend-Endpunkt: GET `/api/disks/scan-folder`.
- **Premium Design-Branding:**
- **Textlogo mit Farbverlauf:** Das "MischLabs"-Textlogo (`.brand h1`) auf dem Dashboard (`index.html`) und im Adminbereich (`admin.html`) erstrahlt nun in einem modernen, dynamischen Farbverlauf von Blau (`#38bdf8`) zu Lila/Flieder (`#c084fc`) unter Verwendung von Text-Maskierungen.
- **Cache-Buster-Update:** Cache-Busting-Parameter in den CSS-Verknüpfungen wurden auf `v=26` erhöht, damit der geänderte Farbverlauf sofort ohne manuelles Cache-Leeren im Browser gerendert wird.
- **Git Deployment & Sync:**
- Sämtliche Implementierungen wurden sicher staged, committet und direkt auf das selbstgehostete Gitea-Remote (`https://git.mischlabs.de/MrDiderot/mischlabs.git`, Branch `master`) gepusht.
- **Frontend Optimierung & Mobile Usability (Stand: 2026-05-22):**
- **PWA-Kachel Entfernung:** Die ungenutzte "PWA Installierbar"-Statistik-Kachel wurde aus der oberen Übersichtsleiste (`index.html`) entfernt. Das Layout der verbleibenden zwei Kacheln ("Online" & "Dienste") wurde in `style.css` auf ein stabiles, zentriertes 2-Spalten-Raster für Desktop und mobile Ansichten angepasst.
- **Kompakte Service-Kategoriefilter:** Die Kategorieauswahl (`.segments`) wurde aus der klebrigen (`sticky`) oberen `.control-bar` herausgelöst und in einem separaten `#categoryFilters`-Bereich direkt über dem Dienste-Grid platziert. Sie verbraucht dadurch im ungescrollten Zustand keinen kostbaren Platz mehr auf Mobilgeräten.
- **Visual-Premium Redesign & Untertitel:** Die Filter-Pills wurden verkleinert (Höhe von 36px auf 28px reduziert, kleinere Schriftgröße `0.78rem` und kompakteres Padding), linksbündig ausgerichtet und mit sanften Hover- & Aktiv-Transitions versehen. Zudem wurde der fälschlicherweise hinzugefügte dunkle Hintergrundbalken auf der `.control-bar` vollständig entfernt, um den nahtlosen Backdrop-Blur-Effekt über dem globalen Farbverlauf wiederherzustellen. Der Untertitel der Landing Page wurde von *"Cloud, Medien, Code..."* auf den prägnanten Namen *"Die Selfhost-Werkstatt"* abgeändert.
- **Favoriten-Zähler statt Online-Status:** Die "Online"-Statistik-Kachel in der Kopfzeile wurde durch eine dynamische "Favoriten"-Statistik-Kachel ersetzt, um direkt die Anzahl der geladenen Lesezeichen anzuzeigen. Die Reihenfolge wurde auf "Dienste" zuerst und "Favoriten" an zweiter Stelle geändert. Das Erreichbarkeits-Monitoring im Hintergrund läuft weiterhin stabil, um die Status-Punkte der einzelnen Dienste-Karten aktuell zu halten.
- **Service Worker API-Bypass & PWA Update:** Der Service-Worker (`sw.js`) ignoriert und reicht nun alle API-Anfragen unter `/api/` (z.B. `/api/favorites`, `/api/status`) direkt an das Netzwerk durch, anstatt sie lokal zu cachen. Dadurch werden neu angelegte Favoriten beim einfachen Navigieren auf das Dashboard sofort und ohne manuelles Strg+F5-Neuladen geladen.
- **Automatisches SSO-Login im Admin Panel:** `/admin.html` prüft nun beim Laden, ob bereits eine aktive Sitzung vorliegt. Falls nicht und kein Fehler vorliegt, wird der Benutzer automatisch an den Keycloak SSO Login (`login()`) weitergeleitet. Dadurch entfällt der zusätzliche Anmelde-Klick komplett. Zum Schutz vor unendlichen Weiterleitungsschleifen nach einer Abmeldung wurde eine temporäre Logout-Erkennung via `sessionStorage` implementiert. Der Cache-Buster für `admin.js` wurde in `admin.html` auf `v=29` erhöht.
- **Cache-Buster & PWA Cache Update:** Die Cache-Busting-Parameter für `style.css` und `app.js` wurden in `index.html` auf `v=33` bzw. `v=40` hochgestuft. Im Service-Worker (`sw.js`) wurde der Cache-Name auf `mischlabs-pwa-v26` erhöht und die App-Shell-Liste entsprechend angepasst, um ein sofortiges Update der App bei allen Nutzern ohne manuelles Cache-Leeren zu garantieren.
- **Header User Badge & Fix CSS Specificity (Stand: 2026-05-22):**
- **Behebung des CSS-Spezifitätskonflikts:** Ein globaler CSS-Reset `[hidden] { display: none !important; }` wurde am Anfang von `style.css` hinzugefügt. Zuvor hat die Spezifität von `.ghost-button { display: inline-flex; }` die Standard-Browserregel für das `hidden`-Attribut überschrieben, was dazu führte, dass sowohl der Login- als auch der Logout-Button fälschlicherweise gleichzeitig dauerhaft angezeigt wurden.
- **Premium Header-User-Badge:** In der Kopfzeile von `admin.html` wurde ein neues Element `<span class="admin-user-badge" id="headerUserBadge" hidden></span>` implementiert, welches bei einer erfolgreichen Authentifizierung in `admin.js` mit dem Text `"Angemeldet als [Nutzername]"` befüllt und stilvoll eingeblendet wird. Der Badge wurde mit einem eleganten Glassmorphismus, dezenter Randlinie und einem 👤-Emoji modern und hochwertig designt.
- **Cache-Buster & PWA Cache Update:** Die Cache-Busting-Parameter für `style.css` wurden in `index.html`, `admin.html` und `favorites.html` auf `v=34` hochgestuft. Im Service-Worker (`sw.js`) wurde der Cache-Name auf `mischlabs-pwa-v27` erhöht, um ein sofortiges Update der App-Shell im Browser aller Anwender zu erzwingen. In `admin.html` wurde der Script-Buster von `admin.js` auf `v=30` erhöht.
- **Migration zu Homepage (gethomepage.dev) & Aufteilung in Public/Admin (Stand: 2026-07-01):**
- **Ablösung des Custom Dashboards:** Das selbstgebaute Node.js-basierte Dashboard (mischlabs.de) wurde vollständig durch die professionelle, open-source Dashboard-Lösung **Homepage (gethomepage.dev)** abgelöst.
- **Trennung in öffentliche und administrative Ansicht:**
- Aus Sicherheitsgründen wurde das Dashboard in zwei separate Homepage-Instanzen aufgeteilt.
- **Öffentliches Dashboard (`mischlabs` / Port 8085):** Verwendet das Konfigurationsverzeichnis `config-public/`. Zeigt nur öffentliche Dienste (Drive, Movies, Audiobooks, Books, MiChess, Tom) ohne sensible Server-Statuswerte oder Administrationslinks.
- **Admin-Dashboard (`mischlabs-admin` / Port 8086):** Verwendet das Konfigurationsverzeichnis `config-admin/`. Zeigt alle 9 Dienste (inkl. Keycloak SSO, Passwords und Git), verfügt über eine Anbindung an den Docker-Socket für Live-Statusanzeigen und liest die NAS-Hardware-Ressourcen (CPU, RAM, Disks) aus.
- **Aktualisierung des Docker-Compose Setups:**
- `docker-compose.yml` (lokal) / `docker-compose.override.yml` (NAS): Definiert beide Homepage-Services und bindet ihre jeweiligen Konfigurationspfade (`./config-public` und `./config-admin`) ein.
- **Deployment-Ergebnis, Styling & Hotfix:** Alle Änderungen wurden erfolgreich in das Gitea-Repository gepusht und direkt auf dem NAS via SSH deployt. Die Host-Validierung wurde mittels `HOMEPAGE_ALLOWED_HOSTS` für beide Instanzen so konfiguriert, dass Zugriffe über Domains (z. B. `mischlabs.de`, `admin.mischlabs.de`) und IP-Adressen autorisiert sind. Zudem wurde ein hochwertiges Glassmorphismus-Design (`custom.css`) und ein atmosphärisches, dunkles Fantasy-Hintergrundbild (`background.png`) für beide Instanzen integriert, um eine harmonische, erstklassige Optik zu erzielen. Beide Instanzen laufen stabil und die Homepage lädt fehlerfrei.

View File

@@ -1,7 +1,29 @@
FROM nginx:alpine
FROM node:22-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
WORKDIR /app
COPY server.js /app/server.js
COPY index.html /app/public/
COPY favorites.html /app/public/
COPY admin.html /app/public/
COPY offline.html /app/public/
COPY style.css /app/public/
COPY app.js /app/public/
COPY auth.js /app/public/
COPY favorites.js /app/public/
COPY admin.js /app/public/
COPY services.json /app/public/
COPY manifest.webmanifest /app/public/
COPY sw.js /app/public/
COPY favicon.ico /app/public/
COPY icons/ /app/public/icons/
ENV NODE_ENV=production
ENV PORT=80
ENV FAVORITES_FILE=/app/data/favorites.json
RUN mkdir -p /app/data
EXPOSE 80
CMD ["node", "server.js"]

249
admin.html Normal file
View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MischLabs Admin</title>
<meta name="description" content="MischLabs dashboard administration.">
<meta name="theme-color" content="#0f172a">
<link rel="manifest" href="/manifest.webmanifest?v=4">
<link rel="shortcut icon" href="/favicon.ico?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32-v4.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
<link rel="stylesheet" href="/style.css?v=35">
</head>
<body>
<header class="shell hero admin-hero">
<div class="brand">
<img class="brand-mark" src="/icons/app-icon-source.png" alt="" width="72" height="72">
<div>
<p class="eyebrow">Admin</p>
<h1>MischLabs</h1>
<p class="subtitle">Dienste pruefen, Kategorien vorbereiten und Dashboard-Konfiguration verwalten.</p>
</div>
</div>
<div class="admin-actions">
<a class="ghost-button" id="headerDashboardBtn" href="/">Dashboard</a>
<button class="ghost-button" id="loginButton" type="button">Mit Keycloak anmelden</button>
<button class="ghost-button" id="logoutButton" type="button" hidden>Abmelden</button>
<span class="admin-user-badge" id="headerUserBadge" hidden></span>
<button class="hamburger-menu-btn" id="hamburgerMenuBtn" aria-label="Menü öffnen" type="button" style="display: none;">
<span class="hamburger-bar"></span>
<span class="hamburger-bar"></span>
<span class="hamburger-bar"></span>
</button>
</div>
</header>
<main class="shell admin-shell">
<section class="admin-panel" id="loginPanel">
<p class="section-kicker">SSO</p>
<h2>Anmeldung erforderlich</h2>
<p class="admin-copy">Diese Seite nutzt Keycloak per Authorization Code + PKCE. Der Client muss in Keycloak als Public Client mit Redirect URI `https://mischlabs.de/admin.html` angelegt sein.</p>
<div class="notice" id="loginMessage">Noch nicht angemeldet.</div>
</section>
<section class="admin-panel" id="adminPanel" hidden>
<div class="admin-panel-head">
<div>
<p class="section-kicker">Konfiguration</p>
<h2>System-Administration</h2>
</div>
</div>
<div id="adminUser" style="display: none;"></div>
<!-- Dashboard Overview Tab -->
<div class="admin-tab-content" id="card-overview">
<div class="overview-grid">
<div class="overview-mini-card" data-target-tab="services">
<div class="mini-card-head">
<span class="mini-card-icon">🌐</span>
<h4>Dienste-Status</h4>
</div>
<div class="mini-card-body" id="mini-services-status">Lade...</div>
</div>
<div class="overview-mini-card" data-target-tab="disks">
<div class="mini-card-head">
<span class="mini-card-icon">💾</span>
<h4>NAS Speicher</h4>
</div>
<div class="mini-card-body" id="mini-disks-status">Lade...</div>
</div>
<div class="overview-mini-card" data-target-tab="watchtower">
<div class="mini-card-head">
<span class="mini-card-icon">🐳</span>
<h4>Watchtower</h4>
</div>
<div class="mini-card-body" id="mini-watchtower-status">Lade...</div>
</div>
<div class="overview-mini-card" data-target-tab="containers">
<div class="mini-card-head">
<span class="mini-card-icon">📦</span>
<h4>Container</h4>
</div>
<div class="mini-card-body" id="mini-containers-status">Lade...</div>
</div>
</div>
</div>
<div class="ops-grid" id="opsGrid">
<section class="ops-card ops-card-wide admin-tab-content" id="card-services">
<div class="ops-card-head">
<div style="display: flex; align-items: center; gap: 8px;">
<span>Dienste-Status (Live-Proxy)</span>
<span id="servicesStatusSummary" class="section-count" style="display: inline; margin-left: 8px; font-weight: 500;"></span>
</div>
<button class="ghost-button mini" id="refreshServicesButton" type="button">Aktualisieren</button>
</div>
<div id="servicesStatusBoard" class="services-status-board">Lade Status...</div>
</section>
<section class="ops-card admin-tab-content" id="card-disks">
<div class="ops-card-head">
<span>NAS Speicher</span>
<button class="ghost-button mini" id="refreshOpsButton" type="button">Aktualisieren</button>
</div>
<div id="diskStatus" class="ops-list">Noch nicht geladen.</div>
<div class="disk-scanner-tool" style="margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border);">
<h4 style="margin-bottom: 8px; color: #fff; font-size: 1.05rem; font-weight: 700;">Verzeichnis-Größen-Scanner</h4>
<p style="font-size: 0.85rem; color: var(--dim); margin-bottom: 14px; line-height: 1.4;">
Scanne Ordnergrößen auf den Host-Volumes (z.B. <code>/host/volume2</code> oder <code>/host/volume1/docker</code>), um Speicherfresser zu finden.
</p>
<div class="scanner-input-group" style="display: flex; gap: 8px; margin-bottom: 16px;">
<input type="text" id="scannerPathInput" value="/host/volume2" placeholder="/host/volume2" style="flex-grow: 1; padding: 10px 14px; background: rgba(0, 0, 0, 0.2); border: 1px solid var(--border); border-radius: var(--radius); color: #fff; font-family: monospace; font-size: 0.9rem;">
<button class="ghost-button mini" id="scannerStartBtn" type="button" style="padding-inline: 16px;">Scan starten</button>
</div>
<div id="scannerResults" class="ops-list" style="max-height: 350px; overflow-y: auto;" hidden></div>
</div>
</section>
<section class="ops-card admin-tab-content" id="card-watchtower">
<div class="ops-card-head">
<span>Watchtower</span>
<div style="display: flex; gap: 8px;">
<button class="ghost-button mini" id="runWatchtowerButton" type="button">Jetzt pruefen</button>
<button class="ghost-button mini" id="viewWatchtowerLogsBtn" type="button">Logs</button>
</div>
</div>
<div id="watchtowerStatus" class="ops-list">Noch nicht geladen.</div>
</section>
<section class="ops-card ops-card-wide admin-tab-content" id="card-containers">
<div class="ops-card-head">
<span>Container</span>
<button class="ghost-button mini danger-button" id="dangerModeButton" type="button">Danger entsperren</button>
</div>
<div class="danger-state" id="dangerState" hidden></div>
<div id="containerStatus" class="ops-list">Noch nicht geladen.</div>
</section>
</div>
<section class="ops-card ops-card-wide admin-tab-content" id="editorCard" style="margin-top: 14px;">
<div class="ops-card-head">
<span>Dienste &amp; Kategorien (Konfiguration)</span>
</div>
<div class="admin-actions-bar" style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;">
<button class="ghost-button mini" id="checkAllButton" type="button">Alle Status pruefen</button>
<button class="ghost-button mini" id="addServiceButton" type="button">Neuen Dienst hinzufügen</button>
<button class="ghost-button mini" id="saveServerButton" type="button" style="background: rgba(56, 189, 248, 0.12); border-color: rgba(56, 189, 248, 0.35); color: #38bdf8;">Auf Server speichern</button>
<button class="ghost-button mini" id="resetLocalButton" type="button">Aenderungen verwerfen</button>
<button class="ghost-button mini" id="downloadConfigButton" type="button">JSON exportieren</button>
</div>
<div id="editorFeedback" class="notice" style="display: none; margin-bottom: 16px;"></div>
<div class="admin-table" id="serviceEditor" style="display: flex; flex-direction: column; gap: 16px;"></div>
</section>
</section>
</main>
<!-- Hamburger Navigation Drawer -->
<div class="admin-nav-backdrop" id="adminNavBackdrop"></div>
<aside class="admin-nav-drawer" id="adminNavDrawer">
<div class="drawer-header">
<h3>MischLabs Admin</h3>
<button class="drawer-close-btn" id="drawerCloseBtn" aria-label="Menü schließen" type="button">&times;</button>
</div>
<nav class="drawer-nav">
<button class="drawer-nav-item" data-tab="overview" type="button">
<span class="drawer-nav-icon">📊</span>
<span class="drawer-nav-text">Übersicht</span>
</button>
<button class="drawer-nav-item" data-tab="services" type="button">
<span class="drawer-nav-icon">🌐</span>
<span class="drawer-nav-text">Dienste-Status</span>
</button>
<button class="drawer-nav-item" data-tab="disks" type="button">
<span class="drawer-nav-icon">💾</span>
<span class="drawer-nav-text">NAS Speicher</span>
</button>
<button class="drawer-nav-item" data-tab="watchtower" type="button">
<span class="drawer-nav-icon">🐳</span>
<span class="drawer-nav-text">Watchtower</span>
</button>
<button class="drawer-nav-item" data-tab="containers" type="button">
<span class="drawer-nav-icon">📦</span>
<span class="drawer-nav-text">Container</span>
</button>
<button class="drawer-nav-item" data-tab="config" type="button">
<span class="drawer-nav-icon">⚙️</span>
<span class="drawer-nav-text">Dienste &amp; Kategorien</span>
</button>
<div class="drawer-divider"></div>
<a class="drawer-nav-item" href="/">
<span class="drawer-nav-icon">🏠</span>
<span class="drawer-nav-text">Dashboard</span>
</a>
<button class="drawer-nav-item drawer-logout-btn" id="drawerLogoutBtn" type="button">
<span class="drawer-nav-icon">🚪</span>
<span class="drawer-nav-text">Abmelden</span>
</button>
</nav>
</aside>
<!-- Container Logs Modal Overlay -->
<div class="modal-overlay" id="logsModalOverlay" hidden>
<div class="modal-card">
<div class="modal-header">
<h3 id="logsModalTitle">Container Logs</h3>
<button class="modal-close-btn" id="logsModalCloseBtn" type="button">&times;</button>
</div>
<div class="modal-body">
<div class="terminal-logs-controls" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; font-size: 0.85rem;">
<div>
<label for="logsTailSelect" style="color: var(--dim); margin-right: 6px;">Zeilen:</label>
<select id="logsTailSelect" style="background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); color: #fff; padding: 4px 8px; cursor: pointer;">
<option value="50">50</option>
<option value="150" selected>150</option>
<option value="300">300</option>
<option value="500">500</option>
</select>
</div>
<button class="ghost-button mini" id="logsModalRefreshBtn" type="button">Aktualisieren</button>
</div>
<pre class="terminal-logs" id="logsModalConsole">Lade Logs...</pre>
</div>
</div>
</div>
<!-- Service Details Modal Overlay -->
<div class="modal-overlay" id="serviceModalOverlay" hidden>
<div class="modal-card">
<div class="modal-header">
<h3 id="serviceModalTitle">Dienst-Inspektion</h3>
<button class="modal-close-btn" id="serviceModalCloseBtn" type="button">&times;</button>
</div>
<div class="modal-body" id="serviceModalContent">
<!-- Dynamically populated details -->
</div>
</div>
</div>
<script src="/admin.js?v=31" defer></script>
</body>
</html>

1493
admin.js Normal file

File diff suppressed because it is too large Load Diff

669
app.js Normal file
View File

@@ -0,0 +1,669 @@
const CONFIG_URL = '/services.json?v=1';
const LOCAL_CONFIG_KEY = 'mischlabs.dashboard.config';
const icons = {
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><path d="M9 12l2 2 4-4"></path>',
folder: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>',
lock: '<rect x="3" y="11" width="18" height="11" rx="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path><circle cx="12" cy="16" r="1"></circle>',
film: '<rect x="2" y="2" width="20" height="20" rx="2.2"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line>',
headphones: '<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path>',
book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>',
git: '<circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line><path d="M12 1.05V7"></path><path d="M12 17.01v5.95"></path>',
chess: '<path d="M8 16l-1 4h10l-1-4"></path><path d="M6.5 16h11"></path><path d="M12 2a2 2 0 0 1 2 2c0 1.1-.9 2-2 3-1.1-1-2-1.9-2-3a2 2 0 0 1 2-2z"></path><path d="M9 7 6 10.5c-.6.7-.2 1.5.7 1.5h10.6c.9 0 1.3-.8.7-1.5L15 7"></path><path d="M9 12v4"></path><path d="M15 12v4"></path>',
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>'
};
let cards = [];
let sections = [];
let config = null;
let favorites = [];
let activeFilter = 'all';
let activeView = window.localStorage.getItem('mischlabs.dashboard.view') || 'all';
let categoryById = new Map();
const searchInput = document.querySelector('#serviceSearch');
const segments = [...document.querySelectorAll('.segment')];
const viewOptions = [...document.querySelectorAll('.view-option')];
const emptyState = document.querySelector('#emptyState');
const favoriteCount = document.querySelector('#favoriteCount');
const serviceCount = document.querySelector('#serviceCount');
const lastChecked = document.querySelector('#lastChecked');
const dashboardSections = document.querySelector('#dashboardSections');
const favoritesSections = document.querySelector('#favoritesSections');
const loginButton = document.querySelector('#dashboardLoginButton');
const logoutButton = document.querySelector('#dashboardLogoutButton');
const favoritesLink = document.querySelector('#favoritesLink');
const userBadge = document.querySelector('#dashboardUserBadge');
const dashboardMenuButton = document.querySelector('#dashboardMenuButton');
const dashboardMenuPanel = document.querySelector('#dashboardMenuPanel');
let adminPanelLink = null;
function normalize(value) {
return String(value || '').toLowerCase().trim();
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getLocalConfig() {
try {
const raw = window.localStorage.getItem(LOCAL_CONFIG_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
async function loadConfig() {
const response = await fetch(CONFIG_URL, { cache: 'no-store' });
const baseConfig = await response.json();
return getLocalConfig() || baseConfig;
}
function serviceSearchText(service) {
return [
service.name,
service.description,
service.domain,
service.category,
service.keywords
].join(' ');
}
function isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
}
function getMobilePlatform() {
const userAgent = navigator.userAgent || '';
if (/Android/i.test(userAgent)) return 'android';
if (/iPhone|iPad|iPod/i.test(userAgent)) return 'ios';
return 'other';
}
function getMobileLaunch(service) {
if (service.id === 'drive') {
const fallbackUrl = service.url;
if (getMobilePlatform() === 'android') {
return {
url: `intent://openFiles#Intent;scheme=nextcloud;package=com.nextcloud.client;S.browser_fallback_url=${encodeURIComponent(fallbackUrl)};end`,
fallbackUrl
};
}
return {
url: 'nextcloud://openFiles',
fallbackUrl
};
}
return null;
}
function openMobileLaunch(url, fallbackUrl) {
let didLeavePage = false;
let fallbackTimer = null;
const cleanup = () => {
window.removeEventListener('pagehide', markPageLeft);
document.removeEventListener('visibilitychange', handleVisibilityChange);
if (fallbackTimer) window.clearTimeout(fallbackTimer);
};
const markPageLeft = () => {
didLeavePage = true;
cleanup();
};
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
markPageLeft();
}
};
window.addEventListener('pagehide', markPageLeft, { once: true });
document.addEventListener('visibilitychange', handleVisibilityChange);
fallbackTimer = window.setTimeout(() => {
cleanup();
if (!didLeavePage && document.visibilityState === 'visible') {
window.location.href = fallbackUrl;
}
}, 2200);
if (/^https?:\/\//i.test(url) || /^intent:/i.test(url)) {
window.location.href = url;
return;
}
window.location.href = url;
}
function createServiceCard(service) {
const category = categoryById.get(service.category);
const card = document.createElement('a');
card.href = service.url;
card.target = '_blank';
card.rel = 'noopener';
card.className = 'card service-card';
card.dataset.category = service.category;
card.dataset.url = service.url;
card.dataset.name = serviceSearchText(service);
card.innerHTML = `
<span class="status-dot" aria-label="Status wird geprueft"></span>
<div class="card-icon" style="--accent: ${service.accent};">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
${icons[service.icon] || icons.folder}
</svg>
</div>
<div class="card-content">
<span class="card-category">${category?.label || 'Dienst'}</span>
<h3>${service.name}</h3>
<p>${service.description}</p>
</div>
<span class="card-domain">${service.domain}</span>
`;
const mobileLaunch = getMobileLaunch(service);
if (mobileLaunch) {
card.addEventListener('click', (e) => {
if (!isMobileDevice()) return;
e.preventDefault();
openMobileLaunch(mobileLaunch.url, mobileLaunch.fallbackUrl);
});
}
return card;
}
function createFavoriteCard(favorite) {
const card = document.createElement('a');
card.href = favorite.url;
card.target = '_blank';
card.rel = 'noopener';
card.className = 'card favorite-card';
card.style.setProperty('--accent', favorite.color || '#67e8f9');
card.dataset.name = [
favorite.name,
favorite.description,
favorite.group,
favorite.url
].join(' ');
const safeLogo = favorite.logoUrl ? escapeHtml(favorite.logoUrl) : '';
const safeName = escapeHtml(favorite.name);
const safeDescription = escapeHtml(favorite.description || favorite.group || 'Favorit');
const iconSvg = icons[favorite.icon] || icons.folder;
card.innerHTML = `
<div class="card-icon favorite-icon" style="--accent: ${favorite.color || '#67e8f9'};">
${safeLogo
? `<img src="${safeLogo}" alt="" loading="lazy">`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${iconSvg}</svg>`}
</div>
<div class="card-content">
<span class="card-category">${escapeHtml(favorite.group || 'Favorit')}</span>
<h3>${safeName}</h3>
<p>${safeDescription}</p>
</div>
<span class="card-domain">${escapeHtml(new URL(favorite.url).hostname)}</span>
`;
return card;
}
function renderFavorites() {
if (favoriteCount) {
favoriteCount.textContent = String(favorites.length);
}
if (!favoritesSections) return;
if (!MischLabsAuth.isAuthenticated()) {
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
</div>
<div class="favorites-empty">
<p>Melde dich mit SSO an, um eigene Kacheln und Weblinks zu speichern.</p>
<button class="ghost-button mini" id="favoritesLoginCallout" type="button">Mit SSO anmelden</button>
</div>
</section>
`;
document.querySelector('#favoritesLoginCallout')?.addEventListener('click', () => MischLabsAuth.login());
updateVisibility();
return;
}
if (!favorites.length) {
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
<a class="ghost-button mini" href="/favorites.html">Favoriten erstellen</a>
</div>
<div class="favorites-empty">
<p>Noch keine Favoriten. Lege eigene Kacheln fuer YouTube, Notion, Tools oder beliebige Links an.</p>
</div>
</section>
`;
updateVisibility();
return;
}
const groups = new Map();
favorites.forEach((favorite) => {
const group = favorite.group || 'Favoriten';
if (!groups.has(group)) groups.set(group, []);
groups.get(group).push(favorite);
});
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
<a class="ghost-button mini" href="/favorites.html">Bearbeiten</a>
</div>
<div class="favorite-groups"></div>
</section>
`;
const container = favoritesSections.querySelector('.favorite-groups');
[...groups.entries()].forEach(([group, items]) => {
const section = document.createElement('section');
section.className = 'favorite-group';
section.innerHTML = `
<div class="favorite-group-heading">
<h3>${escapeHtml(group)}</h3>
<span>${items.length} ${items.length === 1 ? 'Link' : 'Links'}</span>
</div>
<div class="grid favorite-grid"></div>
`;
const grid = section.querySelector('.grid');
items.forEach((favorite) => grid.appendChild(createFavoriteCard(favorite)));
container.appendChild(section);
});
updateVisibility();
}
async function loadFavorites() {
if (!MischLabsAuth.isAuthenticated()) {
favorites = [];
renderFavorites();
return;
}
try {
const response = await fetch('/api/favorites', {
cache: 'no-store',
headers: MischLabsAuth.authHeader()
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
favorites = Array.isArray(payload.favorites) ? payload.favorites : [];
} catch {
favorites = [];
}
renderFavorites();
}
function renderDashboard(nextConfig) {
dashboardSections.innerHTML = '';
serviceCount.textContent = String(nextConfig.services.length);
categoryById = new Map(nextConfig.categories.map((category) => [category.id, category]));
const section = document.createElement('section');
section.className = 'section service-overview';
section.innerHTML = `
<div class="section-heading">
<div>
<p class="section-kicker" id="overviewKicker">Dashboard</p>
<h2 id="overviewTitle">Alle Dienste</h2>
</div>
<span class="section-count" id="overviewCount">${nextConfig.services.length} Dienste</span>
</div>
<div class="grid"></div>
`;
const grid = section.querySelector('.grid');
nextConfig.services.forEach((service) => grid.appendChild(createServiceCard(service)));
dashboardSections.appendChild(section);
cards = [...document.querySelectorAll('.service-card')];
sections = [...document.querySelectorAll('.section')];
updateOverviewHeading(nextConfig.services.length);
}
function updateOverviewHeading(visibleCards) {
const activeCategory = categoryById.get(activeFilter);
const term = normalize(searchInput.value);
const kicker = document.querySelector('#overviewKicker');
const title = document.querySelector('#overviewTitle');
const count = document.querySelector('#overviewCount');
if (!kicker || !title || !count) return;
kicker.textContent = activeCategory ? activeCategory.label : 'Dashboard';
title.textContent = activeCategory ? activeCategory.title : 'Alle Dienste';
count.textContent = `${visibleCards} ${visibleCards === 1 ? 'Dienst' : 'Dienste'}${term ? ' gefunden' : ''}`;
}
function updateVisibility() {
const term = normalize(searchInput.value);
let visibleCards = 0;
let visibleFavorites = 0;
const showServices = activeView === 'all' || activeView === 'services';
const showFavorites = activeView === 'all' || activeView === 'favorites';
if (dashboardSections) {
dashboardSections.hidden = !showServices;
}
const categoryFilters = document.querySelector('#categoryFilters');
if (categoryFilters) {
categoryFilters.hidden = !showServices;
}
if (favoritesSections) {
favoritesSections.hidden = !showFavorites;
}
cards.forEach((card) => {
const categoryMatches = activeFilter === 'all' || card.dataset.category === activeFilter;
const searchMatches = !term || normalize(card.dataset.name).includes(term);
const isVisible = showServices && categoryMatches && searchMatches;
card.hidden = !isVisible;
if (isVisible) visibleCards += 1;
});
sections.forEach((section) => {
const hasVisibleCards = [...section.querySelectorAll('.service-card')].some((card) => !card.hidden);
section.hidden = !showServices || !hasVisibleCards;
});
document.querySelectorAll('.favorite-card').forEach((card) => {
const isVisible = showFavorites && (!term || normalize(card.dataset.name).includes(term));
card.hidden = !isVisible;
if (isVisible) visibleFavorites += 1;
});
document.querySelectorAll('.favorite-group').forEach((group) => {
const hasVisibleCards = [...group.querySelectorAll('.favorite-card')].some((card) => !card.hidden);
group.hidden = !showFavorites || !hasVisibleCards;
});
updateOverviewHeading(visibleCards);
const hasVisibleContent = visibleCards + visibleFavorites > 0;
emptyState.hidden = hasVisibleContent;
emptyState.textContent = term
? 'Kein Eintrag passt zur Suche.'
: 'In dieser Ansicht gibt es noch keine Eintraege.';
}
function setActiveFilter(nextFilter) {
activeFilter = nextFilter;
segments.forEach((segment) => {
const isActive = segment.dataset.filter === nextFilter;
segment.classList.toggle('is-active', isActive);
segment.setAttribute('aria-selected', String(isActive));
});
updateVisibility();
}
function setActiveView(nextView) {
activeView = nextView;
window.localStorage.setItem('mischlabs.dashboard.view', nextView);
viewOptions.forEach((option) => {
const isActive = option.dataset.view === nextView;
option.classList.toggle('is-active', isActive);
option.setAttribute('aria-selected', String(isActive));
});
updateVisibility();
}
async function probeService(card) {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 4500);
try {
await fetch(card.dataset.url, {
method: 'GET',
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal
});
card.classList.add('is-online');
card.classList.remove('is-offline');
card.classList.remove('is-unknown');
card.querySelector('.status-dot').setAttribute('aria-label', 'Online');
return true;
} catch (error) {
const isTimeout = error.name === 'AbortError';
card.classList.toggle('is-offline', isTimeout);
card.classList.toggle('is-unknown', !isTimeout);
card.classList.remove('is-online');
card.querySelector('.status-dot').setAttribute(
'aria-label',
isTimeout ? 'Nicht erreichbar' : 'Im Browser nicht pruefbar'
);
return isTimeout ? false : null;
} finally {
window.clearTimeout(timeout);
}
}
async function refreshStatus() {
const results = await Promise.allSettled(cards.map(probeService));
const unknown = results.filter((result) => result.status === 'fulfilled' && result.value === null).length;
lastChecked.textContent = `Status geprueft: ${new Date().toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
})}${unknown ? `, ${unknown} im Browser nicht pruefbar` : ''}`;
}
function scheduleStatusRefresh() {
const run = () => refreshStatus().catch(() => {
lastChecked.textContent = 'Status aktuell nicht verfuegbar.';
});
if ('requestIdleCallback' in window) {
window.requestIdleCallback(run, { timeout: 2000 });
} else {
window.setTimeout(run, 600);
}
}
segments.forEach((segment) => {
segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter));
});
viewOptions.forEach((option) => {
option.addEventListener('click', () => setActiveView(option.dataset.view));
});
searchInput.addEventListener('input', updateVisibility);
function closeDashboardMenu() {
if (!dashboardMenuButton || !dashboardMenuPanel) return;
dashboardMenuButton.setAttribute('aria-expanded', 'false');
dashboardMenuButton.classList.remove('is-open');
dashboardMenuPanel.classList.remove('is-open');
dashboardMenuPanel.hidden = true;
}
function toggleDashboardMenu() {
if (!dashboardMenuButton || !dashboardMenuPanel) return;
const isOpen = dashboardMenuButton.getAttribute('aria-expanded') === 'true';
dashboardMenuButton.setAttribute('aria-expanded', String(!isOpen));
dashboardMenuButton.classList.toggle('is-open', !isOpen);
dashboardMenuPanel.classList.toggle('is-open', !isOpen);
dashboardMenuPanel.hidden = isOpen;
}
function hasAdminAccess(claims) {
const username = String(claims?.preferred_username || '').toLowerCase();
const email = String(claims?.email || '').toLowerCase();
return username === 'mrdiderot' || email === 'mail.misch@pm.me';
}
function ensureAdminPanelLink() {
if (adminPanelLink || !dashboardMenuPanel) return adminPanelLink;
adminPanelLink = document.createElement('a');
adminPanelLink.className = 'dashboard-menu-item';
adminPanelLink.id = 'adminPanelLink';
adminPanelLink.href = '/admin.html';
adminPanelLink.textContent = 'Admin Panel';
adminPanelLink.hidden = true;
dashboardMenuPanel.insertBefore(adminPanelLink, favoritesLink || loginButton || null);
return adminPanelLink;
}
function updateAuthNav() {
const authenticated = MischLabsAuth.isAuthenticated();
const claims = MischLabsAuth.claims();
const isAdmin = authenticated && hasAdminAccess(claims);
if (loginButton) loginButton.hidden = authenticated;
if (logoutButton) logoutButton.hidden = !authenticated;
if (favoritesLink) favoritesLink.hidden = !authenticated;
if (isAdmin) {
const link = ensureAdminPanelLink();
if (link) link.hidden = false;
} else if (adminPanelLink) {
adminPanelLink.remove();
adminPanelLink = null;
}
if (userBadge) {
userBadge.hidden = !authenticated;
userBadge.textContent = authenticated ? MischLabsAuth.displayName() : '';
}
}
dashboardMenuButton?.addEventListener('click', (event) => {
event.stopPropagation();
toggleDashboardMenu();
});
document.addEventListener('click', (event) => {
if (!dashboardMenuPanel || dashboardMenuPanel.hidden) return;
if (!event.target.closest('.dashboard-menu')) {
closeDashboardMenu();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeDashboardMenu();
}
});
loginButton?.addEventListener('click', () => MischLabsAuth.login());
logoutButton?.addEventListener('click', () => MischLabsAuth.logout());
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
});
}
MischLabsAuth.handleCallback()
.catch((error) => {
lastChecked.textContent = error.message;
})
.then(() => loadConfig())
.then((nextConfig) => {
config = nextConfig;
updateAuthNav();
renderDashboard(config);
setActiveView(activeView);
updateVisibility();
loadFavorites();
scheduleStatusRefresh();
})
.catch(() => {
dashboardSections.innerHTML = '<p class="empty-state">Dashboard-Konfiguration konnte nicht geladen werden.</p>';
if (favoriteCount) {
favoriteCount.textContent = '--';
}
});
// ==========================================================================
// Peeking Bongo Cat Easter Egg Logic
// ==========================================================================
(function initPeekingCat() {
const peekingCat = document.querySelector('#peekingCat');
const searchInput = document.querySelector('#serviceSearch');
if (!peekingCat) return;
let tapTimeout = null;
// Function to trigger heart popup on click
function createHeart(x, y) {
const heart = document.createElement('div');
heart.className = 'cat-heart';
heart.innerHTML = '❤️';
heart.style.left = `${x - 12}px`;
heart.style.top = `${y - 20}px`;
// Set random rotation angle in CSS custom variable
const randomRot = Math.random() * 40 - 20;
heart.style.setProperty('--rot', `${randomRot}deg`);
document.body.appendChild(heart);
// Cleanup heart after animation completes
setTimeout(() => heart.remove(), 800);
}
// Focus search box triggers peeking active state
if (searchInput) {
searchInput.addEventListener('focus', () => {
peekingCat.classList.add('is-active');
});
searchInput.addEventListener('blur', () => {
peekingCat.classList.remove('is-active');
peekingCat.classList.remove('is-tapping');
});
// Keyboard typing triggers tapping animation!
searchInput.addEventListener('input', () => {
peekingCat.classList.add('is-active');
peekingCat.classList.add('is-tapping');
if (tapTimeout) clearTimeout(tapTimeout);
tapTimeout = setTimeout(() => {
peekingCat.classList.remove('is-tapping');
}, 350); // Stop tapping 350ms after typing stops
});
}
// Click on cat triggers tapping + heart animation!
peekingCat.addEventListener('click', (e) => {
peekingCat.classList.add('is-tapping');
createHeart(e.clientX, e.clientY);
if (tapTimeout) clearTimeout(tapTimeout);
tapTimeout = setTimeout(() => {
peekingCat.classList.remove('is-tapping');
}, 800);
});
})();

201
auth.js Normal file
View File

@@ -0,0 +1,201 @@
const MischLabsAuth = (() => {
const TOKEN_KEY = 'mischlabs.dashboard.tokens';
const PKCE_KEY = 'mischlabs.dashboard.pkce';
const CONFIG_KEY = 'mischlabs.dashboard.authConfig';
const SCOPES = 'openid profile email';
let configPromise = null;
let cachedConfig = null;
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
function randomString(length = 64) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return [...bytes].map((byte) => chars[byte % chars.length]).join('');
}
async function createChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
async function getConfig() {
if (cachedConfig) return cachedConfig;
if (!configPromise) {
configPromise = fetch('/api/auth/config', { cache: 'no-store' })
.then((response) => {
if (!response.ok) throw new Error(`Auth config returned ${response.status}`);
return response.json();
})
.then((config) => {
cachedConfig = config;
sessionStorage.setItem(CONFIG_KEY, JSON.stringify(config));
return config;
})
.catch((error) => {
const fallback = JSON.parse(sessionStorage.getItem(CONFIG_KEY) || 'null');
if (fallback) return fallback;
throw error;
});
}
return configPromise;
}
function redirectUri() {
const path = window.location.pathname === '/index.html' ? '/' : window.location.pathname;
return `${window.location.origin}${path}`;
}
function decodeJwt(token) {
const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const json = decodeURIComponent(atob(payload).split('').map((char) => (
`%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
)).join(''));
return JSON.parse(json);
}
function tokens() {
try {
const raw = sessionStorage.getItem(TOKEN_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function storeTokens(nextTokens) {
sessionStorage.setItem(TOKEN_KEY, JSON.stringify(nextTokens));
}
function clearTokens() {
sessionStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(PKCE_KEY);
}
function isAuthenticated() {
const current = tokens();
if (!current?.id_token) return false;
try {
return Date.now() < decodeJwt(current.id_token).exp * 1000;
} catch {
return false;
}
}
function claims() {
const current = tokens();
if (!current?.id_token) return null;
try {
return decodeJwt(current.id_token);
} catch {
return null;
}
}
function authHeader() {
const current = tokens();
return current?.id_token ? { Authorization: `Bearer ${current.id_token}` } : {};
}
async function login() {
const config = await getConfig();
const verifier = randomString();
const challenge = await createChallenge(verifier);
const state = randomString(32);
const redirect = redirectUri();
sessionStorage.setItem(PKCE_KEY, JSON.stringify({ verifier, state, redirect }));
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirect,
response_type: 'code',
scope: config.scopes || SCOPES,
state,
code_challenge: challenge,
code_challenge_method: 'S256'
});
window.location.href = `${config.authority}/protocol/openid-connect/auth?${params}`;
}
async function handleCallback() {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const error = url.searchParams.get('error');
if (error) {
window.history.replaceState({}, document.title, redirectUri());
throw new Error(`SSO Fehler: ${error}`);
}
if (!code) return false;
const config = await getConfig();
const stored = JSON.parse(sessionStorage.getItem(PKCE_KEY) || '{}');
if (!stored.verifier || stored.state !== state) {
throw new Error('SSO Antwort konnte nicht verifiziert werden.');
}
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
redirect_uri: stored.redirect || redirectUri(),
code,
code_verifier: stored.verifier
});
const response = await fetch(`${config.authority}/protocol/openid-connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
if (!response.ok) {
throw new Error(`Token endpoint returned ${response.status}`);
}
storeTokens(await response.json());
sessionStorage.removeItem(PKCE_KEY);
window.history.replaceState({}, document.title, redirectUri());
return true;
}
async function logout() {
const config = await getConfig();
const idToken = tokens()?.id_token;
clearTokens();
const params = new URLSearchParams({
client_id: config.clientId,
post_logout_redirect_uri: redirectUri()
});
if (idToken) params.set('id_token_hint', idToken);
window.location.href = `${config.authority}/protocol/openid-connect/logout?${params}`;
}
function displayName() {
const currentClaims = claims();
return currentClaims?.name || currentClaims?.preferred_username || currentClaims?.email || 'Angemeldet';
}
return {
authHeader,
claims,
clearTokens,
displayName,
getConfig,
handleCallback,
isAuthenticated,
login,
logout,
tokens
};
})();

74
brain.old Normal file
View File

@@ -0,0 +1,74 @@
# Brain.md - Projekt mischlabs.de Landing Page
## Projektziel
Eine ansprechende Landing Page fuer mischlabs.de, die alle Dienste als Kacheln mit direkten Verlinkungen anzeigt. Laeuft als Docker-Container und wird ueber Git auto-deployed.
## Status
- [x] Git-Repo initialisiert und Remote verbunden (https://git.mischlabs.de/MrDiderot/mischlabs.git)
- [x] Dienste-Liste vom Screenshot erfasst (9 Services inkl. MiChess)
- [x] Landing Page Design und Implementierung (index.html + style.css)
- [x] Docker-Setup (Dockerfile, docker-compose.yml, nginx.conf)
- [x] Push zum Remote
- [x] MiChess-Kachel hinzugefuegt (Commit lokal, Push steht aus wg. Auth-Fehler)
- [ ] Push des MiChess-Commits (Git-Auth-Token erneuern)
## Verwandte Projekte
- **auth** (d:\Vibecoding\Website\auth) - Keycloak SSO fuer alle Dienste
- Repo: https://git.mischlabs.de/MrDiderot/auth.git
- Subdomain: auth.mischlabs.de
- Siehe auth/Brain.md fuer Details
## Dienste-Uebersicht (8 Stueck)
| # | Subdomain | Beschreibung | Farbe/Accent |
|---|-----------|-------------|--------------|
| 1 | drive.mischlabs.de | Cloud-Speicher | #4285f4 (Blau) |
| 2 | git.mischlabs.de | Code-Repositories | #f05033 (Rot) |
| 3 | movies.mischlabs.de | Filme & Serien | #a855f7 (Lila) |
| 4 | audiobook.mischlabs.de | Hoerbuecher | #22c55e (Gruen) |
| 5 | password.mischlabs.de | Passwort-Manager | #eab308 (Gelb) |
| 6 | docs.mischlabs.de | Dokumentation | #06b6d4 (Cyan) |
| 7 | michess.mischlabs.de | Schach | #10b981 (Gruen) |
| 8 | tom.mischlabs.de | Persoenliche Seite | #ec4899 (Pink) |
## Technische Entscheidungen
- **Reines HTML + CSS** (kein Framework, kein JS) - maximale Performance, minimales Docker-Image
- **nginx:alpine** als Base-Image - extrem leichtgewichtig (~5MB)
- **Google Fonts (Inter)** fuer moderne Typografie
- **Inline SVG Icons** - keine externen Dependencies
- **CSS Grid** mit `auto-fill` + `minmax(240px, 1fr)` fuer responsives Layout
- **Dark Theme** mit subtilen Glow-Effekten
- **Staggered Animations**: Kacheln faden nacheinander ein (9 Stueck)
## Docker-Setup
- **Container-Name**: `mischlabs`
- **Base-Image**: `nginx:alpine`
- **Port-Mapping**: `8085:80` (anpassbar in docker-compose.yml)
- **Restart-Policy**: `unless-stopped`
- **nginx.conf**: Caching fuer Static Assets (7 Tage), Security-Headers
## Dateistruktur
```
mischlabs/
index.html - Haupt-HTML mit allen 9 Service-Kacheln
style.css - Komplettes Styling (Dark Theme, Grid, Animationen)
Dockerfile - nginx:alpine mit kopierten Static Files
docker-compose.yml - Container-Definition (Name: mischlabs, Port 8085)
nginx.conf - Server-Konfiguration, Caching, Security-Headers
.gitignore - Git-Ausschluesse
Brain.md - Dieses Dokument (KI-Uebergabe)
```
## Deployment-Workflow
1. Code aendern und committen
2. Push zu git.mischlabs.de
3. Auf dem Server: `git pull && docker compose up -d --build`
## Erkenntnisse & Notizen
- Alle Dienste laufen auf 192.168.178.88 mit verschiedenen Ports
- Oeffentlich erreichbar ueber *.mischlabs.de Subdomains (Cloudflare Tunnel)
- Port 8085 gewaehlt da gaengige Ports (80, 8080, 8090) bereits belegt
- Kein JavaScript noetig - pure CSS reicht fuer alle Effekte
- `color-mix()` CSS-Funktion fuer dynamische Icon-Hintergruende
- Git-Push-Fehler aufgetreten (Authentication failed) - Token muss erneuert werden
- DNS: Alte A-Records (185.181.104.242) von INWX wurden geloescht, nur noch Tunnel-Eintraege
- NAS hat kein git installiert - entweder Git Server Paket installieren oder wget nutzen

BIN
config-admin/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

46
config-admin/custom.css Normal file
View File

@@ -0,0 +1,46 @@
/* Glassmorphism theme override */
.theme-dark {
--theme-bg: transparent !important;
}
body {
background-image: url('/background.png') !important;
background-size: cover !important;
background-position: center !important;
background-attachment: fixed !important;
background-repeat: no-repeat !important;
background-color: #0f172a !important;
}
/* Set the layout wrapper to be 85% dominant dark slate, letting the background peek through subtly (15% visibility) */
#__next, #__next > div, main, .layout, #layout-wrapper {
background-color: rgba(15, 23, 42, 0.85) !important;
}
/* Semi-transparent blur cards */
.card, .widget, .resource-widget {
background-color: rgba(30, 41, 59, 0.45) !important;
backdrop-filter: blur(10px) saturate(120%) !important;
-webkit-backdrop-filter: blur(10px) saturate(120%) !important;
border: 1px solid rgba(255, 255, 255, 0.05) !important;
border-radius: 12px !important;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important;
transition: all 0.3s ease !important;
}
.card:hover {
background-color: rgba(51, 65, 85, 0.55) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
transform: translateY(-2px) !important;
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5) !important;
}
/* Style headers and category names */
h2, h3 {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5) !important;
}
/* Resources widget style matching the glass theme */
.resource-widget {
padding: 12px !important;
}

2
config-admin/docker.yaml Normal file
View File

@@ -0,0 +1,2 @@
local:
socket: /var/run/docker.sock

103
config-admin/services.yaml Normal file
View File

@@ -0,0 +1,103 @@
- Zugang:
- Single Sign-On:
icon: keycloak.png
href: https://auth.mischlabs.de/realms/mischlabs/account
description: Account, Sitzungen und Sicherheit
server: local
container: auth
- Passwords:
icon: vaultwarden.png
href: https://password.mischlabs.de
description: Passwort-Manager
server: local
container: vaultwarden
- Medien:
- Drive:
icon: nextcloud.png
href: https://drive.mischlabs.de
description: Cloud-Speicher
server: local
container: nextcloud_app
- Movies:
icon: jellyfin.png
href: http://192.168.178.111:8096
description: Filme & Serien (VM)
siteMonitor: http://192.168.178.111:8096
- Plex:
icon: plex.png
href: http://192.168.178.111:32400
description: Plex Media Server (VM)
siteMonitor: http://192.168.178.111:32400
- Audiobooks:
icon: audiobookshelf.png
href: https://audiobook.mischlabs.de
description: Hörbücher
server: local
container: audiobookshelf
- Books:
icon: calibre-web.png
href: https://books.mischlabs.de
description: Bücher & Bibliothek
server: local
container: calibre-web
- Servarr (VM):
- Jellyseerr:
icon: jellyseerr.png
href: http://192.168.178.111:5055
description: Medienanfragen verwalten
siteMonitor: http://192.168.178.111:5055
- Sonarr:
icon: sonarr.png
href: http://192.168.178.111:8989
description: Serien verwalten
siteMonitor: http://192.168.178.111:8989
- Radarr:
icon: radarr.png
href: http://192.168.178.111:7878
description: Filme verwalten
siteMonitor: http://192.168.178.111:7878
- Prowlarr:
icon: prowlarr.png
href: http://192.168.178.111:9696
description: Indexer Manager
siteMonitor: http://192.168.178.111:9696
- SABnzbd:
icon: sabnzbd.png
href: http://192.168.178.111:8080
description: Usenet Downloader
siteMonitor: http://192.168.178.111:8080
- Arbeit & Admin:
- Git:
icon: gitea.png
href: https://git.mischlabs.de
description: Code-Repositories
server: local
container: gitea
- Config-Editor:
icon: filebrowser.png
href: https://editor.mischlabs.de
description: Dashboard Config bearbeiten
server: local
container: mischlabs-editor
- Portainer (VM):
icon: portainer.png
href: https://192.168.178.111:9443
description: VM Container verwalten
siteMonitor: https://192.168.178.111:9443
- Persönlich:
- MiChess:
icon: lichess.png
href: https://michess.mischlabs.de
description: Schach spielen
server: local
container: michess
- Tom:
icon: user-circle.png
href: https://tom.mischlabs.de
description: Portfolio & Fotos
server: local
container: photography-website

View File

@@ -0,0 +1,5 @@
title: MischLabs Admin
theme: dark
language: de
tlsInsecure: true

12
config-admin/widgets.yaml Normal file
View File

@@ -0,0 +1,12 @@
- search:
provider: duckduckgo
focus: true
target: _blank
- resources:
label: NAS Status
cpu: true
memory: true
disk:
- /host/volume1
- /host/volume2

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

46
config-public/custom.css Normal file
View File

@@ -0,0 +1,46 @@
/* Glassmorphism theme override */
.theme-dark {
--theme-bg: transparent !important;
}
body {
background-image: url('/background.png') !important;
background-size: cover !important;
background-position: center !important;
background-attachment: fixed !important;
background-repeat: no-repeat !important;
background-color: #0f172a !important;
}
/* Set the layout wrapper to be 85% dominant dark slate, letting the background peek through subtly (15% visibility) */
#__next, #__next > div, main, .layout, #layout-wrapper {
background-color: rgba(15, 23, 42, 0.85) !important;
}
/* Semi-transparent blur cards */
.card, .widget, .resource-widget {
background-color: rgba(30, 41, 59, 0.45) !important;
backdrop-filter: blur(10px) saturate(120%) !important;
-webkit-backdrop-filter: blur(10px) saturate(120%) !important;
border: 1px solid rgba(255, 255, 255, 0.05) !important;
border-radius: 12px !important;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important;
transition: all 0.3s ease !important;
}
.card:hover {
background-color: rgba(51, 65, 85, 0.55) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
transform: translateY(-2px) !important;
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5) !important;
}
/* Style headers and category names */
h2, h3 {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5) !important;
}
/* Resources widget style matching the glass theme */
.resource-widget {
padding: 12px !important;
}

View File

@@ -0,0 +1,33 @@
- Medien:
- Drive:
icon: nextcloud.png
href: https://drive.mischlabs.de
description: Cloud-Speicher
- Movies:
icon: jellyfin.png
href: http://192.168.178.111:8096
description: Filme & Serien
siteMonitor: http://192.168.178.111:8096
- Audiobooks:
icon: audiobookshelf.png
href: https://audiobook.mischlabs.de
description: Hörbücher
- Books:
icon: calibre-web.png
href: https://books.mischlabs.de
description: Bücher & Bibliothek
- Plex:
icon: plex.png
href: http://192.168.178.111:32400
description: Plex Media Server
siteMonitor: http://192.168.178.111:32400
- Persönlich:
- MiChess:
icon: lichess.png
href: https://michess.mischlabs.de
description: Schach spielen
- Tom:
icon: user-circle.png
href: https://tom.mischlabs.de
description: Portfolio & Fotos

View File

@@ -0,0 +1,4 @@
title: MischLabs
theme: dark
language: de
tlsInsecure: true

View File

@@ -0,0 +1,4 @@
- search:
provider: duckduckgo
focus: true
target: _blank

View File

@@ -1,7 +1,45 @@
services:
mischlabs:
build: .
image: ghcr.io/gethomepage/homepage:latest
container_name: mischlabs
restart: unless-stopped
ports:
- "8085:80"
- "8085:3000"
environment:
HOMEPAGE_ALLOWED_HOSTS: mischlabs.de,www.mischlabs.de,192.168.178.88,192.168.178.88:8085,localhost,127.0.0.1
volumes:
- ./config-public:/app/config
- ./config-public/background.png:/app/public/background.png:ro
- mischlabs_data:/app/data
mischlabs-admin:
image: ghcr.io/gethomepage/homepage:latest
container_name: mischlabs-admin
restart: unless-stopped
ports:
- "8086:3000"
environment:
HOMEPAGE_ALLOWED_HOSTS: admin.mischlabs.de,192.168.178.88,192.168.178.88:8086,localhost,127.0.0.1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /volume1:/host/volume1:ro
- /volume2:/host/volume2:ro
- ./config-admin:/app/config
- ./config-admin/background.png:/app/public/background.png:ro
- mischlabs_admin_data:/app/data
mischlabs-editor:
image: filebrowser/filebrowser:latest
container_name: mischlabs-editor
restart: unless-stopped
ports:
- "8087:80"
volumes:
- .:/srv
- ./filebrowser.db:/database/filebrowser.db
volumes:
mischlabs_data:
mischlabs_admin_data:

199
docs/Brain.md Normal file
View File

@@ -0,0 +1,199 @@
# Brain.md - Projekt mischlabs.de Landing Page
## Projektziel
Eine ansprechende Landing Page fuer mischlabs.de, die alle Dienste als Kacheln mit direkten Verlinkungen anzeigt. Laeuft als Docker-Container und wird ueber Git auto-deployed.
Aktueller Stand: Die Seite ist keine reine Linkliste mehr, sondern ein kompaktes Service-Dashboard mit Kategorien, Suche, Hintergrund-Statuscheck und PWA-Unterstuetzung.
## Status
- [x] Git-Repo initialisiert und Remote verbunden (https://git.mischlabs.de/MrDiderot/mischlabs.git)
- [x] Dienste-Liste vom Screenshot erfasst (9 Services inkl. MiChess)
- [x] Landing Page Design und Implementierung (index.html + style.css)
- [x] Docker-Setup (Dockerfile, docker-compose.yml, nginx.conf)
- [x] PWA-Grundlage fuer iOS/Home-Screen-Installation
- [x] Dashboard v2 mit Kategorien, Suche und Live-Statusindikatoren
- [x] Push zum Remote
- [x] MiChess-Kachel hinzugefuegt (Commit lokal, Push steht aus wg. Auth-Fehler)
- [ ] Push des MiChess-Commits (Git-Auth-Token erneuern)
## Verwandte Projekte
- **auth** (d:\Vibecoding\Website\auth) - Keycloak SSO fuer alle Dienste
- Repo: https://git.mischlabs.de/MrDiderot/auth.git
- Subdomain: auth.mischlabs.de
- Siehe auth/Brain.md fuer Details
## Dienste-Uebersicht (8 Stueck)
| # | Subdomain | Beschreibung | Farbe/Accent |
|---|-----------|-------------|--------------|
| 1 | drive.mischlabs.de | Cloud-Speicher | #4285f4 (Blau) |
| 2 | git.mischlabs.de | Code-Repositories | #f05033 (Rot) |
| 3 | movies.mischlabs.de | Filme & Serien | #a855f7 (Lila) |
| 4 | audiobook.mischlabs.de | Hoerbuecher | #22c55e (Gruen) |
| 5 | password.mischlabs.de | Passwort-Manager | #eab308 (Gelb) |
| 6 | docs.mischlabs.de | Dokumentation | #06b6d4 (Cyan) |
| 7 | michess.mischlabs.de | Schach | #10b981 (Gruen) |
| 8 | tom.mischlabs.de | Persoenliche Seite | #ec4899 (Pink) |
## Technische Entscheidungen
- **Reines HTML + CSS mit minimalem Dashboard-/PWA-JS** (kein Framework) - maximale Performance, minimales Docker-Image
- **nginx:alpine** als Base-Image - extrem leichtgewichtig (~5MB)
- **Google Fonts (Inter)** fuer moderne Typografie
- **Inline SVG Icons** - keine externen Dependencies
- **CSS Grid** mit `auto-fill` + `minmax(240px, 1fr)` fuer responsives Layout
- **Dark Theme** mit subtilen Glow-Effekten
- **Staggered Animations**: Kacheln faden nacheinander ein (9 Stueck)
- **PWA**: Manifest, Apple-Touch-Icon, App-Icons und Service Worker fuer installierbare iOS/Web-App
- **Dashboard**: Kategorien `Zugang`, `Medien`, `Arbeit`, `Persoenlich`, Suche und clientseitige no-cors Statuschecks
- **Auth-Link**: Zeigt auf die Keycloak Account Console (`/realms/mischlabs/account`), nicht mehr auf die Admin-Konsole
- **Adminseite**: `/admin.html` nutzt Keycloak Authorization Code + PKCE fuer Login und bietet Editor/Statusdiagnose fuer Dienste
## Docker-Setup
- **Container-Name**: `mischlabs`
- **Base-Image**: `nginx:alpine`
- **Port-Mapping**: `8085:80` (anpassbar in docker-compose.yml)
- **Restart-Policy**: `unless-stopped`
- **nginx.conf**: Caching fuer Static Assets (7 Tage), Security-Headers
## Dateistruktur
```
mischlabs/
index.html - Haupt-HTML mit allen 9 Service-Kacheln
style.css - Komplettes Styling (Dark Theme, Grid, Animationen)
app.js - Suche, Filter, Statuschecks, Service-Worker-Registrierung
admin.html - SSO-geschuetzte Adminoberflaeche
admin.js - Keycloak PKCE Login, Editor, Statusdiagnose
services.json - Zentrale Service-/Kategorie-Konfiguration
offline.html - Offline-Fallback fuer installierte PWA
manifest.webmanifest - PWA-Metadaten fuer installierbare App
sw.js - Schlanker Service Worker mit App-Shell-Cache
icons/ - App-Icons fuer iOS/PWA
Dockerfile - nginx:alpine mit kopierten Static Files
docker-compose.yml - Container-Definition (Name: mischlabs, Port 8085)
nginx.conf - Server-Konfiguration, Caching, Security-Headers
.gitignore - Git-Ausschluesse
Brain.md - Dieses Dokument (KI-Uebergabe)
```
## Deployment-Workflow
1. Code aendern und committen
2. Push zu git.mischlabs.de
3. Auf dem Server: `git pull && docker compose up -d --build`
## Erkenntnisse & Notizen
- Alle Dienste laufen auf 192.168.178.88 mit verschiedenen Ports
- Oeffentlich erreichbar ueber *.mischlabs.de Subdomains (Cloudflare Tunnel)
- Port 8085 gewaehlt da gaengige Ports (80, 8080, 8090) bereits belegt
- Kein JavaScript noetig - pure CSS reicht fuer alle Effekte
- `color-mix()` CSS-Funktion fuer dynamische Icon-Hintergruende
- Git-Push-Fehler aufgetreten (Authentication failed) - Token muss erneuert werden
- DNS: Alte A-Records (185.181.104.242) von INWX wurden geloescht, nur noch Tunnel-Eintraege
- NAS hat kein git installiert - entweder Git Server Paket installieren oder wget nutzen
## PWA-Stand 2026-05-21
- MischLabs wurde als PWA vorbereitet.
- iOS-relevante Meta-Tags in `index.html`:
- `apple-mobile-web-app-capable`
- `apple-mobile-web-app-status-bar-style`
- `apple-mobile-web-app-title`
- `apple-touch-icon`
- Manifest:
- `name`: MischLabs
- `display`: standalone
- `theme_color`: #10b981
- `background_color`: #0a0a0f
- Icons:
- `icons/app-icon-source.png` (1024x1024, quadratischer Ausschnitt aus dem MischLabs-Bild)
- `icons/apple-touch-icon-v2.png` (180x180, frischer Dateiname gegen iOS-Icon-Cache)
- `icons/apple-touch-icon.png` (180x180, alte Kompatibilitaetskopie)
- `icons/icon-192.png` (192x192)
- `icons/icon-512.png` (512x512)
- `icons/maskable-512.png` (512x512, maskable)
- Service Worker:
- Cache-Name: `mischlabs-pwa-v4`
- Cached die App-Shell (`/`, `index.html`, `offline.html`, `style.css?v=10`, `app.js?v=2`, `services.json?v=1`, Manifest und Icons)
- Navigationen fallen offline auf `offline.html` zurueck
- Dockerfile kopiert Manifest, Service Worker und Icons in das Nginx-Image.
- `nginx.conf` liefert `manifest.webmanifest` mit `application/manifest+json` und `sw.js` mit harten No-Cache-Headern aus.
## Dashboard-v2-Stand 2026-05-21
- Neues Logo oben nutzt `icons/app-icon-source.png`.
- Die alte oeffentliche Auth-Kachel wurde auf die Keycloak Account Console umgebogen:
`https://auth.mischlabs.de/realms/mischlabs/account`
- Kategorien:
- Zugang: SSO, Drive, Passwords
- Medien: Movies, Audiobooks, Books
- Arbeit: Git, MiChess
- Persoenlich: Tom
- `app.js` prueft Dienste im Hintergrund per `fetch(..., { mode: "no-cors" })`. Das ist bewusst nur ein Erreichbarkeitsindikator, kein vollwertiges Monitoring.
## Adminseite Stand 2026-05-21
- URL: `https://mischlabs.de/admin.html`
- SSO-Client in Keycloak benoetigt:
- Client ID: `mischlabs-admin`
- Client type: OpenID Connect
- Client authentication: Off/Public Client
- Standard flow: On
- Valid redirect URI: `https://mischlabs.de/admin.html`
- Valid post logout redirect URI: `https://mischlabs.de/admin.html`
- Web origin: `https://mischlabs.de`
- Scopes: `openid email profile`
- Erlaubter Admin in der statischen UI:
- `preferred_username=mrdiderot` oder `email=mail.misch@pm.me`
- Aktuelle Grenze: Weil mischlabs noch rein statisch ueber Nginx laeuft, kann `/admin.html` globale Aenderungen noch nicht serverseitig speichern. Der Editor speichert lokal im Browser (`localStorage`) und kann eine neue `services.json` exportieren. Fuer echte Live-Aenderungen braucht der naechste Schritt ein kleines Backend oder einen Gitea-Commit-Workflow.
- Statusdiagnose nutzt Browser-Fetch mit `mode: "no-cors"`. Dadurch erkennt sie Erreichbarkeit/Timeouts, aber bei fremden Subdomains keine echten HTTP-Statuscodes.
- Wenn der Browser die Pruefung wegen CORS/CORB blockiert (`Failed to fetch`), wird der Dienst **nicht** mehr als offline markiert, sondern als "Nicht im Browser pruefbar". Das betraf z. B. Vaultwarden/`password.mischlabs.de`.
- Fuer echte Fehlerdetails braucht es ein kleines Backend bzw. einen Statusproxy, der serverseitig `HEAD`/`GET` prueft.
## Statusproxy Stand 2026-05-21
- `mischlabs` wurde von Nginx auf einen kleinen Node-Server (`server.js`) umgestellt.
- Der Node-Server serviert die statischen Dateien weiter und stellt `/api/status` bereit.
- `/api/status` liefert:
- Docker-Container via `/var/run/docker.sock`
- Watchtower-Logs und letzten Watchtower-Lauf
- Speicherplatz fuer `/volume1` und `/volume2`, im Container gemountet als `/host/volume1` und `/host/volume2`
- Serverseitige Service-Checks mit HTTP-Status und Latenz
- NAS-Compose fuer `mischlabs` braucht diese Mounts:
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /volume1:/host/volume1:ro
- /volume2:/host/volume2:ro
```
- Aktuelle NAS-Werte bei Planung:
- `/volume1`: 3.7T total, 2.5T used, 1.2T free, 68%
- `/volume2`: 104G total, 88G used, 12G free, 89%
- Watchtower: `Scanned=23`, `Updated=0`, `Failed=0` beim manuellen Lauf um 2026-05-21 14:16 UTC
## Globale KI-Systemeinstellung (WICHTIG!)
> [!IMPORTANT]
> **Arbeitsanweisung für alle KI-Systeme (LLM / Agenten):**
> Nach dem erfolgreichen Abschluss einer Aufgabe oder eines Meilensteins MUSS automatisch ein strukturierter Eintrag in dieser `Brain.md` vorgenommen werden. Dieser Eintrag soll dokumentieren:
> 1. Welche Änderungen an welchen Dateien vorgenommen wurden (inklusive Begründung).
> 2. Welche neuen Features, Endpoints oder Design-Anpassungen integriert wurden.
> 3. Der aktuelle Stand des Deployments und eventuelle offene Punkte oder nächste Schritte.
> Dies stellt sicher, dass alle nachfolgenden KI-Systeme und der Benutzer stets einen konsistenten, lückenlosen Projektverlauf nachvollziehen können.
## Letzte Änderungen & Meilensteine (Stand: 2026-05-21)
- **Erweitertes Admin-Tool (Universal Admin Panel):**
- **Docker Container Logs Modal:** Integration eines interaktiven, terminal-ähnlichen Log-Overlays in `admin.html` und `admin.js`. Jeder Container auf der Übersicht sowie Watchtower bieten nun einen **Logs**-Button. Logs können dynamisch mit Tiefenbeschränkungen (`50`, `150`, `300`, `500` Zeilen) abgefragt und manuell aktualisiert werden. Backend-Endpunkt: GET `/api/containers/:name/logs`.
- **Live Service-Inspektion (`🔍`):** Status-Kacheln der Dienste bieten ein Lupensymbol. Klicks darauf starten eine serverseitige Node-Anfrage (CORS-frei) an die Zieladresse und geben Antwortlatenz, HTTP-Statuscode, vollständige HTTP-Header und eine Vorschau der ersten 1000 Zeichen der Antwort zurück. Backend-Endpunkt: GET `/api/services/inspect`.
- **Interaktiver NAS Verzeichnis-Scanner:** Hinzufügen eines Ordnergrößen-Scanners im NAS-Speicher-Panel. Berechnet rekursiv Ordnergrößen für gemountete Volumes (`/host/volume1`, `/host/volume2`), sortiert sie absteigend und erlaubt durch Anklicken von Unterordnern ein direktes Weiterscannen im entsprechenden Pfad. Backend-Endpunkt: GET `/api/disks/scan-folder`.
- **Premium Design-Branding:**
- **Textlogo mit Farbverlauf:** Das "MischLabs"-Textlogo (`.brand h1`) auf dem Dashboard (`index.html`) und im Adminbereich (`admin.html`) erstrahlt nun in einem modernen, dynamischen Farbverlauf von Blau (`#38bdf8`) zu Lila/Flieder (`#c084fc`) unter Verwendung von Text-Maskierungen.
- **Cache-Buster-Update:** Cache-Busting-Parameter in den CSS-Verknüpfungen wurden auf `v=26` erhöht, damit der geänderte Farbverlauf sofort ohne manuelles Cache-Leeren im Browser gerendert wird.
- **Git Deployment & Sync:**
- Sämtliche Implementierungen wurden sicher staged, committet und direkt auf das selbstgehostete Gitea-Remote (`https://git.mischlabs.de/MrDiderot/mischlabs.git`, Branch `master`) gepusht.
- **Frontend Optimierung & Mobile Usability (Stand: 2026-05-22):**
- **PWA-Kachel Entfernung:** Die ungenutzte "PWA Installierbar"-Statistik-Kachel wurde aus der oberen Übersichtsleiste (`index.html`) entfernt. Das Layout der verbleibenden zwei Kacheln ("Online" & "Dienste") wurde in `style.css` auf ein stabiles, zentriertes 2-Spalten-Raster für Desktop und mobile Ansichten angepasst.
- **Kompakte Service-Kategoriefilter:** Die Kategorieauswahl (`.segments`) wurde aus der klebrigen (`sticky`) oberen `.control-bar` herausgelöst und in einem separaten `#categoryFilters`-Bereich direkt über dem Dienste-Grid platziert. Sie verbraucht dadurch im ungescrollten Zustand keinen kostbaren Platz mehr auf Mobilgeräten.
- **Visual-Premium Redesign & Untertitel:** Die Filter-Pills wurden verkleinert (Höhe von 36px auf 28px reduziert, kleinere Schriftgröße `0.78rem` und kompakteres Padding), linksbündig ausgerichtet und mit sanften Hover- & Aktiv-Transitions versehen. Zudem wurde der fälschlicherweise hinzugefügte dunkle Hintergrundbalken auf der `.control-bar` vollständig entfernt, um den nahtlosen Backdrop-Blur-Effekt über dem globalen Farbverlauf wiederherzustellen. Der Untertitel der Landing Page wurde von *"Cloud, Medien, Code..."* auf den prägnanten Namen *"Die Selfhost-Werkstatt"* abgeändert.
- **Favoriten-Zähler statt Online-Status:** Die "Online"-Statistik-Kachel in der Kopfzeile wurde durch eine dynamische "Favoriten"-Statistik-Kachel ersetzt, um direkt die Anzahl der geladenen Lesezeichen anzuzeigen. Die Reihenfolge wurde auf "Dienste" zuerst und "Favoriten" an zweiter Stelle geändert. Das Erreichbarkeits-Monitoring im Hintergrund läuft weiterhin stabil, um die Status-Punkte der einzelnen Dienste-Karten aktuell zu halten.
- **Service Worker API-Bypass & PWA Update:** Der Service-Worker (`sw.js`) ignoriert und reicht nun alle API-Anfragen unter `/api/` (z.B. `/api/favorites`, `/api/status`) direkt an das Netzwerk durch, anstatt sie lokal zu cachen. Dadurch werden neu angelegte Favoriten beim einfachen Navigieren auf das Dashboard sofort und ohne manuelles Strg+F5-Neuladen geladen.
- **Automatisches SSO-Login im Admin Panel:** `/admin.html` prüft nun beim Laden, ob bereits eine aktive Sitzung vorliegt. Falls nicht und kein Fehler vorliegt, wird der Benutzer automatisch an den Keycloak SSO Login (`login()`) weitergeleitet. Dadurch entfällt der zusätzliche Anmelde-Klick komplett. Zum Schutz vor unendlichen Weiterleitungsschleifen nach einer Abmeldung wurde eine temporäre Logout-Erkennung via `sessionStorage` implementiert. Der Cache-Buster für `admin.js` wurde in `admin.html` auf `v=29` erhöht.
- **Cache-Buster & PWA Cache Update:** Die Cache-Busting-Parameter für `style.css` und `app.js` wurden in `index.html` auf `v=33` bzw. `v=40` hochgestuft. Im Service-Worker (`sw.js`) wurde der Cache-Name auf `mischlabs-pwa-v26` erhöht und die App-Shell-Liste entsprechend angepasst, um ein sofortiges Update der App bei allen Nutzern ohne manuelles Cache-Leeren zu garantieren.
- **Header User Badge & Fix CSS Specificity (Stand: 2026-05-22):**
- **Behebung des CSS-Spezifitätskonflikts:** Ein globaler CSS-Reset `[hidden] { display: none !important; }` wurde am Anfang von `style.css` hinzugefügt. Zuvor hat die Spezifität von `.ghost-button { display: inline-flex; }` die Standard-Browserregel für das `hidden`-Attribut überschrieben, was dazu führte, dass sowohl der Login- als auch der Logout-Button fälschlicherweise gleichzeitig dauerhaft angezeigt wurden.
- **Premium Header-User-Badge:** In der Kopfzeile von `admin.html` wurde ein neues Element `<span class="admin-user-badge" id="headerUserBadge" hidden></span>` implementiert, welches bei einer erfolgreichen Authentifizierung in `admin.js` mit dem Text `"Angemeldet als [Nutzername]"` befüllt und stilvoll eingeblendet wird. Der Badge wurde mit einem eleganten Glassmorphismus, dezenter Randlinie und einem 👤-Emoji modern und hochwertig designt.
- **Cache-Buster & PWA Cache Update:** Die Cache-Busting-Parameter für `style.css` wurden in `index.html`, `admin.html` und `favorites.html` auf `v=34` hochgestuft. Im Service-Worker (`sw.js`) wurde der Cache-Name auf `mischlabs-pwa-v27` erhöht, um ein sofortiges Update der App-Shell im Browser aller Anwender zu erzwingen. In `admin.html` wurde der Script-Buster von `admin.js` auf `v=30` erhöht.

65
docs/Mischlabs Aendern.md Normal file
View File

@@ -0,0 +1,65 @@
# 2. Wie ändere ich Dienste, Widgets oder Einstellungen?
Die gesamte Homepage wird über YAML-Dateien konfiguriert. Du musst dafür keinen Code schreiben oder das Docker-Image neu bauen. Homepage lädt diese Dateien im laufenden Betrieb automatisch neu!
---
## 1. Dienste verwalten (services.yaml)
Hier legst du die Kacheln für deine Dienste fest.
### Beispiel für einen einfachen Dienst (Öffentlich):
```yaml
- Medien: # Dies ist die Kategorie-Überschrift
- Drive: # Name des Dienstes auf der Kachel
icon: nextcloud.png # Icon-Name (aus dem Homepage-Iconset)
href: https://drive.mischlabs.de
description: Cloud-Speicher
```
### Beispiel mit Live-Docker-Status (Nur für Admin):
```yaml
- Arbeit:
- Git:
icon: gitea.png
href: https://git.mischlabs.de
description: Code-Repositories
server: local # Verweist auf docker.yaml
container: gitea # Exakter Name des Containers auf dem NAS
```
*Tipp: Wenn du `server: local` und `container: [name]` angibst, fragt Homepage den Docker-Status ab und zeigt an, ob der Container läuft (grüner Punkt) oder beendet ist (roter Punkt).*
*Eine Liste aller unterstützten App-Icons findest du in der [Homepage Dokumentation](https://gethomepage.dev/configs/services/).*
---
## 2. Einstellungen anpassen (settings.yaml)
Hier werden grundlegende Einstellungen wie Sprache und Titel definiert:
```yaml
title: MischLabs Admin # Der Name oben links im Dashboard
theme: dark # Standard-Theme (dark oder light)
language: de # Sprache für Datumsangaben und Container-Zustände (z.B. "Wird ausgeführt")
```
---
## 3. Widgets verwalten (widgets.yaml)
Widgets werden ganz oben auf der Seite als schmale Infoleisten oder Suchboxen angezeigt.
### Beispiel für Suchleiste + Hardware-Widgets (Admin):
```yaml
- search:
provider: duckduckgo # Suchanbieter (google, duckduckgo, etc.)
focus: true # Suchbox bei Seitenaufruf fokussieren
- resources:
label: NAS Status
cpu: true
memory: true
disk:
- /host/volume1 # Überwacht Volume 1 des NAS
- /host/volume2 # Überwacht Volume 2 des NAS
```
---
* [[Mischlabs Struktur|Zurück: Struktur]] | [[Mischlabs Anleitung|Zurück zur Übersicht]] | [[Mischlabs Deployment|Weiter zu: Deployment]]

View File

@@ -0,0 +1,12 @@
# Mischlabs Dashboard Anleitung
Willkommen im Handbuch für dein neues Homepage-Dashboard!
Da deine Homepage nun auf **Homepage (gethomepage.dev)** basiert, ist die Verwaltung deutlich einfacher und professioneller geworden. Das System ist in zwei Instanzen aufgeteilt (Öffentlich und Admin).
Hier findest du die einzelnen Themen als separate Notizen, die dich durch die Verwaltung führen:
* [[Mischlabs Struktur|1. Struktur der Homepage (Wo liegt was?)]]
* [[Mischlabs Aendern|2. Wie ändere ich Dienste, Widgets oder Einstellungen?]]
* [[Mischlabs Deployment|3. Wie werden Änderungen auf das NAS übertragen (Deployment)?]]
* [[Mischlabs Editor|4. Dashboard-Editor nutzen (Filebrowser)]]

View File

@@ -0,0 +1,42 @@
# 3. Wie wird die Seite deployed / aktualisiert?
Wenn du Änderungen an deinen Config-Dateien (in `config-public` oder `config-admin`) oder an der Compose-Datei vornimmst, müssen diese auf das NAS übertragen werden.
Dank des eingerichteten SSH-Schlüssels auf deinem Windows-Rechner kannst du das bequem über die Windows PowerShell erledigen.
---
## Schritt-für-Schritt-Anleitung
### Schritt 1: Lokale Änderungen machen & Git sichern
1. Bearbeite die Dateien lokal in deinem Git-Repository auf deinem Windows-Rechner (`d:\Vibecoding\Website\mischlabs`).
2. Öffne ein Terminal in diesem Ordner und sichere deine Änderungen im Git (Gitea):
```bash
git add .
git commit -m "Beschreibung deiner Änderung"
git push
```
---
### Schritt 2: Änderungen auf das NAS hochladen (Deployment)
Du musst die Dateien nicht manuell hochladen. Öffne die **PowerShell** auf deinem Windows-Rechner und führe folgende Befehle aus:
#### 1. Nur die YAML-Konfigurationen synchronisieren:
```powershell
scp -r -i C:\Users\tommi\.ssh\codex_mischnas_ed25519 d:\Vibecoding\Website\mischlabs\config-public d:\Vibecoding\Website\mischlabs\config-admin MrDiderot@192.168.178.88:/volume2/docker/mischlabs/
```
*Da Homepage Dateiänderungen automatisch überwacht, sind deine Änderungen im Browser **sofort live**! Du musst den Docker-Container bei reinen Inhaltsänderungen nicht neu starten.*
#### 2. Falls sich die docker-compose.yml geändert hat:
Wenn du z.B. Ports oder Mount-Pfade änderst, lade die Datei hoch und starte die Container neu:
```powershell
# Compose-Datei hochladen
scp -i C:\Users\tommi\.ssh\codex_mischnas_ed25519 d:\Vibecoding\Website\mischlabs\docker-compose.yml MrDiderot@192.168.178.88:/volume2/docker/mischlabs/docker-compose.override.yml
# Container auf dem NAS neu starten
ssh -i C:\Users\tommi\.ssh\codex_mischnas_ed25519 MrDiderot@192.168.178.88 "cd /volume2/docker/mischlabs && docker compose down mischlabs && docker compose up -d mischlabs mischlabs-admin"
```
---
* [[Mischlabs Aendern|Zurück: Dienste ändern]] | [[Mischlabs Anleitung|Zurück zur Übersicht]]

28
docs/Mischlabs Editor.md Normal file
View File

@@ -0,0 +1,28 @@
# 4. Dashboard-Editor nutzen (Filebrowser)
Du hast jetzt einen integrierten Web-Editor (**Filebrowser**), den du direkt über dein Admin-Dashboard aufrufen kannst. Damit kannst du deine Dashboards verwalten, ohne dich per SSH einloggen oder Git-Befehle nutzen zu müssen.
## Zugriff & Login
* **Adresse:** `http://192.168.178.88:8087` (oder falls eingerichtet: `https://editor.mischlabs.de`)
* **Standard-Benutzername:** `admin`
* **Einmal-Passwort (für den ersten Login):** `kd5hzSdaRcsHoq_c`
> [!IMPORTANT]
> Bitte ändere das Passwort direkt nach deinem ersten Login in den Filebrowser-Einstellungen (unten links auf das Zahnrad-Symbol -> *Settings* -> *Change Password*).
---
## Wie bearbeite ich die Homepage?
1. Nach dem Login siehst du die Ordnerstruktur deines Homepage-Projekts.
2. Navigiere in die Config-Ordner:
* **Öffentlich:** Ordner `config-public`
* **Admin:** Ordner `config-admin`
3. Klicke auf die Datei, die du bearbeiten möchtest (z.B. `services.yaml` oder `settings.yaml`).
4. Der Editor öffnet sich direkt im Browser mit Syntax-Hervorhebung.
5. Nimm deine Änderungen vor und klicke oben rechts auf das **Speichern-Symbol** (die Diskette).
6. Deine Änderungen sind **sofort live** im Dashboard sichtbar!
---
* [[Mischlabs Anleitung|Zurück zur Übersicht]]

View File

@@ -0,0 +1,40 @@
# 1. Struktur der Homepage
Deine Homepage besteht aus zwei getrennten Instanzen, um deine privaten Dienste (wie Passwörter oder Gitea) und Server-Statistiken vor der Öffentlichkeit zu schützen.
## Verzeichnisstruktur (Lokal & NAS)
In deinem Projektverzeichnis findest du zwei Hauptordner für die Konfigurationen:
```
mischlabs/
├── config-public/ # Konfiguration für die öffentliche Seite (Port 8085)
│ ├── settings.yaml # Titel, Theme und Sprache
│ ├── services.yaml # Nur öffentliche Dienste (Drive, Movies, etc.)
│ └── widgets.yaml # Nur die Suchleiste
├── config-admin/ # Konfiguration für die Admin-Seite (Port 8086)
│ ├── settings.yaml # Titel, Theme und Sprache
│ ├── services.yaml # Alle 9 Dienste mit Docker-Status
│ ├── widgets.yaml # Suchleiste + NAS Hardware-Auslastung
│ └── docker.yaml # Anbindung an den Docker-Socket
└── docker-compose.yml # Container-Konfiguration (Image, Ports, Volumes)
```
---
## Die zwei Instanzen im Detail
### 1. Öffentliche Homepage (mischlabs)
* **Domain:** `https://mischlabs.de` (Port `8085` auf dem NAS)
* **Sicherheit:** Hat **keinen** Zugriff auf den Docker-Socket oder deine NAS-Verzeichnisgrößen.
* **Inhalt:** Zeigt nur Services, die einen eigenen Login haben (Nextcloud, Jellyfin, Audiobookshelf, Calibre-Web, Lichess, Tom).
### 2. Admin-Homepage (mischlabs-admin)
* **Domain:** `https://admin.mischlabs.de` (Port `8086` auf dem NAS)
* **Sicherheit:** Sollte über Cloudflare Access oder VPN geschützt sein.
* **Inhalt:** Zeigt alle 9 Dienste inkl. Keycloak SSO, Vaultwarden Passwörter und Gitea.
* **Widgets:** Zeigt live CPU-, RAM- und Festplatten-Auslastung deines NAS an.
---
* [[Mischlabs Anleitung|Zurück zur Übersicht]] | [[Mischlabs Aendern|Weiter zu: Wie ändere ich Dienste oder Einstellungen?]]

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

73
favorites.html Normal file
View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MischLabs - Favoriten</title>
<meta name="description" content="Persoenliche MischLabs Favoriten verwalten.">
<meta name="theme-color" content="#0f172a">
<link rel="manifest" href="/manifest.webmanifest?v=4">
<link rel="shortcut icon" href="/favicon.ico?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32-v4.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
<link rel="stylesheet" href="/style.css?v=34">
</head>
<body>
<header class="shell hero admin-hero">
<div class="brand">
<img class="brand-mark" src="/icons/app-icon-source.png" alt="" width="72" height="72">
<div>
<p class="eyebrow">Persoenlich</p>
<h1>Favoriten</h1>
<p class="subtitle">Eigene Weblinks als Kacheln auf deinem MischLabs Dashboard.</p>
</div>
</div>
<div class="admin-actions">
<a class="ghost-button" href="/">Dashboard</a>
<button class="ghost-button" id="favoritesLoginButton" type="button">Mit SSO anmelden</button>
<button class="ghost-button" id="favoritesLogoutButton" type="button" hidden>Abmelden</button>
</div>
</header>
<main class="shell favorites-shell">
<section class="admin-panel" id="favoritesLoginPanel">
<p class="section-kicker">SSO</p>
<h2>Anmeldung erforderlich</h2>
<p class="admin-copy">Deine Favoriten werden pro Keycloak-Benutzer gespeichert.</p>
<div class="notice" id="favoritesMessage">Noch nicht angemeldet.</div>
</section>
<section class="admin-panel" id="favoritesPanel" hidden>
<div class="admin-panel-head">
<div>
<p class="section-kicker">Editor</p>
<h2>Favoriten verwalten</h2>
<p class="admin-copy">Lege Kacheln an, waehle Farbe und Logo, gruppiere sie und sortiere die Reihenfolge.</p>
</div>
<button class="ghost-button" id="addFavoriteButton" type="button">Favorit hinzufuegen</button>
</div>
<div class="notice" id="favoritesUser"></div>
<div class="favorite-editor-layout">
<div class="favorite-editor-list" id="favoriteEditor"></div>
<aside class="favorite-preview-panel">
<p class="section-kicker">Vorschau</p>
<div id="favoritePreview" class="grid favorite-preview-grid"></div>
</aside>
</div>
<div class="admin-actions-bar">
<button class="ghost-button" id="saveFavoritesButton" type="button">Speichern</button>
<button class="ghost-button" id="reloadFavoritesButton" type="button">Neu laden</button>
</div>
</section>
</main>
<script src="/auth.js?v=2" defer></script>
<script src="/favorites.js?v=1" defer></script>
</body>
</html>

239
favorites.js Normal file
View File

@@ -0,0 +1,239 @@
const favoriteIcons = {
star: '<polygon points="12 2 15 8.5 22 9.3 16.8 14 18.2 21 12 17.4 5.8 21 7.2 14 2 9.3 9 8.5 12 2"></polygon>',
link: '<path d="M10 13a5 5 0 0 0 7.1 0l2.8-2.8a5 5 0 0 0-7.1-7.1l-1.6 1.6"></path><path d="M14 11a5 5 0 0 0-7.1 0l-2.8 2.8a5 5 0 0 0 7.1 7.1l1.6-1.6"></path>',
play: '<circle cx="12" cy="12" r="10"></circle><path d="m10 8 6 4-6 4z"></path>',
note: '<path d="M4 3h11l5 5v13H4z"></path><path d="M14 3v6h6"></path><path d="M8 13h8"></path><path d="M8 17h6"></path>',
book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>',
code: '<path d="m16 18 6-6-6-6"></path><path d="m8 6-6 6 6 6"></path>',
mail: '<rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m3 7 9 6 9-6"></path>',
calendar: '<rect x="3" y="4" width="18" height="18" rx="2"></rect><path d="M16 2v4"></path><path d="M8 2v4"></path><path d="M3 10h18"></path>',
image: '<rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="m21 15-5-5L5 21"></path>',
tool: '<path d="M14.7 6.3a4 4 0 0 0-5 5L3 18v3h3l6.7-6.7a4 4 0 0 0 5-5l-2.8 2.8-2-2z"></path>'
};
const iconLabels = {
star: 'Stern',
link: 'Link',
play: 'Video',
note: 'Notiz',
book: 'Buch',
code: 'Code',
mail: 'Mail',
calendar: 'Kalender',
image: 'Bild',
tool: 'Tool'
};
const loginButton = document.querySelector('#favoritesLoginButton');
const logoutButton = document.querySelector('#favoritesLogoutButton');
const loginPanel = document.querySelector('#favoritesLoginPanel');
const panel = document.querySelector('#favoritesPanel');
const message = document.querySelector('#favoritesMessage');
const userBox = document.querySelector('#favoritesUser');
const editor = document.querySelector('#favoriteEditor');
const preview = document.querySelector('#favoritePreview');
const addButton = document.querySelector('#addFavoriteButton');
const saveButton = document.querySelector('#saveFavoritesButton');
const reloadButton = document.querySelector('#reloadFavoritesButton');
let favorites = [];
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function createDefaultFavorite() {
return {
id: crypto.randomUUID(),
name: 'Neuer Link',
url: 'https://',
group: 'Favoriten',
description: '',
color: '#67e8f9',
icon: 'star',
logoUrl: '',
order: favorites.length
};
}
function iconOptions(selected) {
return Object.keys(favoriteIcons).map((icon) => (
`<option value="${icon}" ${icon === selected ? 'selected' : ''}>${iconLabels[icon]}</option>`
)).join('');
}
function syncFromInputs() {
editor.querySelectorAll('.favorite-edit-card').forEach((row) => {
const favorite = favorites[Number(row.dataset.index)];
if (!favorite) return;
row.querySelectorAll('[data-field]').forEach((input) => {
favorite[input.dataset.field] = input.value;
});
});
}
function moveFavorite(index, direction) {
const next = index + direction;
if (next < 0 || next >= favorites.length) return;
syncFromInputs();
[favorites[index], favorites[next]] = [favorites[next], favorites[index]];
favorites = favorites.map((favorite, order) => ({ ...favorite, order }));
render();
}
function removeFavorite(index) {
syncFromInputs();
favorites.splice(index, 1);
favorites = favorites.map((favorite, order) => ({ ...favorite, order }));
render();
}
function renderPreviewCard(favorite) {
const color = /^#[0-9a-fA-F]{6}$/.test(favorite.color) ? favorite.color : '#67e8f9';
const iconSvg = favoriteIcons[favorite.icon] || favoriteIcons.star;
let host = '';
try {
host = new URL(favorite.url).hostname;
} catch {
host = 'ungueltige-url';
}
return `
<article class="card favorite-card" style="--accent: ${color};">
<div class="card-icon favorite-icon" style="--accent: ${color};">
${favorite.logoUrl
? `<img src="${escapeHtml(favorite.logoUrl)}" alt="" loading="lazy">`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${iconSvg}</svg>`}
</div>
<div class="card-content">
<span class="card-category">${escapeHtml(favorite.group || 'Favorit')}</span>
<h3>${escapeHtml(favorite.name)}</h3>
<p>${escapeHtml(favorite.description || 'Eigener Weblink')}</p>
</div>
<span class="card-domain">${escapeHtml(host)}</span>
</article>
`;
}
function renderPreview() {
preview.innerHTML = favorites.map(renderPreviewCard).join('') || '<div class="favorites-empty">Die Vorschau erscheint hier.</div>';
}
function render() {
editor.innerHTML = favorites.map((favorite, index) => `
<article class="favorite-edit-card" data-index="${index}">
<div class="favorite-edit-head">
<strong>${escapeHtml(favorite.name || 'Neuer Link')}</strong>
<div class="favorite-order-actions">
<button class="ghost-button mini" data-action="up" type="button" ${index === 0 ? 'disabled' : ''}>Hoch</button>
<button class="ghost-button mini" data-action="down" type="button" ${index === favorites.length - 1 ? 'disabled' : ''}>Runter</button>
<button class="ghost-button mini danger-button" data-action="remove" type="button">Entfernen</button>
</div>
</div>
<div class="favorite-form-grid">
<label>Beschriftung<input data-field="name" value="${escapeHtml(favorite.name)}" placeholder="YouTube"></label>
<label>Link<input data-field="url" value="${escapeHtml(favorite.url)}" placeholder="https://youtube.com"></label>
<label>Gruppe<input data-field="group" value="${escapeHtml(favorite.group)}" placeholder="Privat"></label>
<label>Beschreibung<input data-field="description" value="${escapeHtml(favorite.description)}" placeholder="Optionaler Untertitel"></label>
<label>Farbe<input data-field="color" type="color" value="${escapeHtml(favorite.color || '#67e8f9')}"></label>
<label>Icon<select data-field="icon">${iconOptions(favorite.icon || 'star')}</select></label>
<label class="favorite-form-wide">Logo URL<input data-field="logoUrl" value="${escapeHtml(favorite.logoUrl)}" placeholder="https://.../logo.png"></label>
</div>
</article>
`).join('') || '<div class="favorites-empty">Noch keine Favoriten angelegt.</div>';
renderPreview();
editor.querySelectorAll('[data-field]').forEach((input) => {
input.addEventListener('input', () => {
syncFromInputs();
renderPreview();
});
});
editor.querySelectorAll('[data-action]').forEach((button) => {
button.addEventListener('click', () => {
const index = Number(button.closest('.favorite-edit-card').dataset.index);
if (button.dataset.action === 'up') moveFavorite(index, -1);
if (button.dataset.action === 'down') moveFavorite(index, 1);
if (button.dataset.action === 'remove') removeFavorite(index);
});
});
}
async function loadFavorites() {
const response = await fetch('/api/favorites', {
cache: 'no-store',
headers: MischLabsAuth.authHeader()
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
favorites = Array.isArray(payload.favorites) ? payload.favorites : [];
userBox.textContent = `Angemeldet als ${payload.user?.name || MischLabsAuth.displayName()}`;
render();
}
async function saveFavorites() {
syncFromInputs();
const response = await fetch('/api/favorites', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...MischLabsAuth.authHeader()
},
body: JSON.stringify({ favorites })
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
favorites = payload.favorites || [];
render();
userBox.textContent = 'Favoriten gespeichert.';
}
function updateAuthUi() {
const authenticated = MischLabsAuth.isAuthenticated();
loginButton.hidden = authenticated;
logoutButton.hidden = !authenticated;
loginPanel.hidden = authenticated;
panel.hidden = !authenticated;
}
async function boot() {
try {
await MischLabsAuth.handleCallback();
} catch (error) {
message.textContent = error.message;
}
updateAuthUi();
if (MischLabsAuth.isAuthenticated()) {
await loadFavorites();
}
}
loginButton.addEventListener('click', () => MischLabsAuth.login());
logoutButton.addEventListener('click', () => MischLabsAuth.logout());
addButton.addEventListener('click', () => {
syncFromInputs();
favorites.push(createDefaultFavorite());
render();
});
saveButton.addEventListener('click', () => {
saveFavorites().catch((error) => {
userBox.textContent = `Speichern fehlgeschlagen: ${error.message}`;
});
});
reloadButton.addEventListener('click', () => {
loadFavorites().catch((error) => {
userBox.textContent = `Laden fehlgeschlagen: ${error.message}`;
});
});
boot().catch((error) => {
message.textContent = `Favoritenseite konnte nicht starten: ${error.message}`;
});

BIN
icons/app-icon-source.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
icons/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
icons/favicon-192-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
icons/favicon-32-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
icons/favicon-48-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
icons/favicon-512-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

BIN
icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

BIN
icons/maskable-512-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

BIN
icons/maskable-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

View File

@@ -3,153 +3,144 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MischLabs - Services</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>MischLabs - Dashboard</title>
<meta name="description" content="MischLabs service dashboard for self-hosted apps.">
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="MischLabs">
<link rel="manifest" href="/manifest.webmanifest?v=4">
<link rel="shortcut icon" href="/favicon.ico?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32-v4.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
<link rel="stylesheet" href="style.css?v=34">
</head>
<body>
<div class="bg-glow bg-glow-1"></div>
<div class="bg-glow bg-glow-2"></div>
<header>
<div class="logo">
<span class="logo-icon">&#9881;</span>
<h1>Misch<span>Labs</span></h1>
<header class="shell hero">
<div class="brand">
<img class="brand-mark" src="/icons/app-icon-source.png" alt="" width="72" height="72">
<div>
<p class="eyebrow">Self-hosted dashboard</p>
<h1>MischLabs</h1>
<p class="subtitle">Die Selfhost-Werkstatt</p>
</div>
</div>
<p class="subtitle">Self-Hosted Services</p>
<div class="hero-meta" aria-label="Systemuebersicht">
<div class="metric">
<span class="metric-value" id="serviceCount">9</span>
<span class="metric-label">Dienste</span>
</div>
<div class="metric">
<span class="metric-value" id="favoriteCount">--</span>
<span class="metric-label">Favoriten</span>
</div>
</div>
</header>
<main>
<div class="grid">
<a href="https://drive.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #4285f4;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<main class="shell">
<section class="control-bar" aria-label="Dienste filtern">
<label class="search-box">
<span class="search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7"></circle>
<path d="m20 20-3.5-3.5"></path>
</svg>
</span>
<input id="serviceSearch" type="search" placeholder="Dienst suchen" autocomplete="off">
<div class="dashboard-menu">
<button class="dashboard-menu-button" id="dashboardMenuButton" type="button" aria-label="Menue oeffnen" aria-expanded="false" aria-controls="dashboardMenuPanel">
<span></span>
<span></span>
<span></span>
</button>
<nav class="dashboard-menu-panel" id="dashboardMenuPanel" aria-label="Dashboard Aktionen" hidden>
<span class="dashboard-menu-user" id="dashboardUserBadge" hidden></span>
<a class="dashboard-menu-item" id="favoritesLink" href="/favorites.html" hidden>Favoriten verwalten</a>
<button class="dashboard-menu-item" id="dashboardLoginButton" type="button">SSO Login</button>
<button class="dashboard-menu-item is-danger" id="dashboardLogoutButton" type="button" hidden>Abmelden</button>
</nav>
</div>
<div class="card-content">
<h2>Drive</h2>
<p>Cloud-Speicher</p>
</div>
<span class="card-domain">drive.mischlabs.de</span>
</a>
</label>
<a href="https://git.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #f05033;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/>
<path d="M12 1.05V7"/><path d="M12 17.01v5.95"/>
</svg>
</div>
<div class="card-content">
<h2>Git</h2>
<p>Code-Repositories</p>
</div>
<span class="card-domain">git.mischlabs.de</span>
</a>
<a href="https://movies.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #a855f7;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
<line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/>
<line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/>
<line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/>
<line x1="17" y1="7" x2="22" y2="7"/>
</svg>
</div>
<div class="card-content">
<h2>Movies</h2>
<p>Filme &amp; Serien</p>
</div>
<span class="card-domain">movies.mischlabs.de</span>
</a>
<a href="https://audiobook.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #22c55e;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
</svg>
</div>
<div class="card-content">
<h2>Audiobooks</h2>
<p>Hoerbuecher</p>
</div>
<span class="card-domain">audiobook.mischlabs.de</span>
</a>
<a href="https://foodlabs.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #f97316;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8h1a4 4 0 0 1 0 8h-1"/>
<path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/>
<line x1="6" y1="1" x2="6" y2="4"/>
<line x1="10" y1="1" x2="10" y2="4"/>
<line x1="14" y1="1" x2="14" y2="4"/>
</svg>
</div>
<div class="card-content">
<h2>FoodLabs</h2>
<p>Rezepte</p>
</div>
<span class="card-domain">foodlabs.mischlabs.de</span>
</a>
<a href="https://password.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #eab308;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
<circle cx="12" cy="16" r="1"/>
</svg>
</div>
<div class="card-content">
<h2>Passwords</h2>
<p>Passwort-Manager</p>
</div>
<span class="card-domain">password.mischlabs.de</span>
</a>
<a href="https://docs.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #06b6d4;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</div>
<div class="card-content">
<h2>Docs</h2>
<p>Dokumentation</p>
</div>
<span class="card-domain">docs.mischlabs.de</span>
</a>
<a href="https://tom.mischlabs.de" class="card" target="_blank" rel="noopener">
<div class="card-icon" style="--accent: #ec4899;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<div class="card-content">
<h2>Tom</h2>
<p>Persoenliche Seite</p>
</div>
<span class="card-domain">tom.mischlabs.de</span>
</a>
<div class="view-toggle" role="tablist" aria-label="Ansicht">
<button class="view-option is-active" type="button" data-view="all">Alles</button>
<button class="view-option" type="button" data-view="services">Dienste</button>
<button class="view-option" type="button" data-view="favorites">Favoriten</button>
</div>
</section>
<!-- Compact categories filter options placed directly above the service grid -->
<div id="categoryFilters" class="category-filters">
<div class="segments" role="tablist" aria-label="Kategorien">
<button class="segment is-active" type="button" data-filter="all">Alle</button>
<button class="segment" type="button" data-filter="access">Zugang</button>
<button class="segment" type="button" data-filter="media">Medien</button>
<button class="segment" type="button" data-filter="work">Arbeit</button>
<button class="segment" type="button" data-filter="personal">Persoenlich</button>
</div>
</div>
<div id="dashboardSections"></div>
<div id="favoritesSections"></div>
<p class="empty-state" id="emptyState" hidden>Kein Dienst passt zur Suche.</p>
</main>
<footer>
<p>&copy; 2026 MischLabs &mdash; Self-Hosted with care</p>
<footer class="shell footer">
<span>MischLabs</span>
<span id="lastChecked">Status wird im Hintergrund geprueft.</span>
</footer>
<!-- Peeking Bongo Cat Easter Egg -->
<div class="peeking-cat" id="peekingCat" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" width="120" height="100">
<!-- Left Ear -->
<path class="cat-ear cat-ear-left" d="M 25 50 L 10 15 L 45 35 Z" fill="#ffffff" stroke="#1e293b" stroke-width="3" stroke-linejoin="round" />
<path class="cat-ear cat-ear-left-inner" d="M 28 45 L 17 21 L 40 34 Z" fill="#fecdd3" />
<!-- Right Ear -->
<path class="cat-ear cat-ear-right" d="M 95 50 L 110 15 L 75 35 Z" fill="#ffffff" stroke="#1e293b" stroke-width="3" stroke-linejoin="round" />
<path class="cat-ear cat-ear-right-inner" d="M 92 45 L 103 21 L 80 34 Z" fill="#fecdd3" />
<!-- Face/Head -->
<path class="cat-head" d="M 15 70 C 15 25, 105 25, 105 70 L 15 70 Z" fill="#ffffff" stroke="#1e293b" stroke-width="3" stroke-linejoin="round" />
<!-- Cheeks -->
<ellipse class="cat-blush" cx="28" cy="58" rx="8" ry="5" fill="#fca5a5" opacity="0.6" />
<ellipse class="cat-blush" cx="92" cy="58" rx="8" ry="5" fill="#fca5a5" opacity="0.6" />
<!-- Eyes -->
<g class="cat-eye-group">
<ellipse class="cat-eye cat-eye-l" cx="42" cy="50" rx="4" ry="5" fill="#1e293b" />
<circle class="cat-eye-shine cat-eye-shine-l" cx="40" cy="48" r="1.2" fill="#ffffff" />
</g>
<g class="cat-eye-group">
<ellipse class="cat-eye cat-eye-r" cx="78" cy="50" rx="4" ry="5" fill="#1e293b" />
<circle class="cat-eye-shine cat-eye-shine-r" cx="76" cy="48" r="1.2" fill="#ffffff" />
</g>
<!-- Nose/Mouth -->
<path class="cat-mouth" d="M 57 54 Q 60 57 63 54 Q 66 57 69 54" fill="none" stroke="#1e293b" stroke-width="2.5" stroke-linecap="round" />
<!-- Whiskers -->
<line class="cat-whisker" x1="12" y1="52" x2="3" y2="50" stroke="#1e293b" stroke-width="2" stroke-linecap="round" />
<line class="cat-whisker" x1="11" y1="58" x2="2" y2="58" stroke="#1e293b" stroke-width="2" stroke-linecap="round" />
<line class="cat-whisker" x1="108" y1="52" x2="117" y2="50" stroke="#1e293b" stroke-width="2" stroke-linecap="round" />
<line class="cat-whisker" x1="109" y1="58" x2="118" y2="58" stroke="#1e293b" stroke-width="2" stroke-linecap="round" />
<!-- Paws -->
<path class="cat-paw cat-paw-left" d="M 30 70 C 30 58, 46 58, 46 70 Z" fill="#ffffff" stroke="#1e293b" stroke-width="3" stroke-linejoin="round" />
<path class="cat-paw cat-paw-right" d="M 74 70 C 74 58, 90 58, 90 70 Z" fill="#ffffff" stroke="#1e293b" stroke-width="3" stroke-linejoin="round" />
</svg>
</div>
<script src="/auth.js?v=2" defer></script>
<script src="/app.js?v=40" defer></script>
</body>
</html>

33
manifest.webmanifest Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "MischLabs",
"short_name": "MischLabs",
"description": "Self-hosted service dashboard for MischLabs.",
"lang": "de",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0a0a0f",
"theme_color": "#0f172a",
"categories": ["productivity", "utilities"],
"icons": [
{
"src": "/icons/favicon-192-v4.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/favicon-512-v4.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/maskable-512-v4.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

View File

@@ -9,6 +9,22 @@ server {
try_files $uri $uri/ /index.html;
}
location = /manifest.webmanifest {
default_type application/manifest+json;
add_header Cache-Control "public, max-age=3600" always;
}
location = /services.json {
default_type application/json;
add_header Cache-Control "no-cache" always;
}
location = /sw.js {
default_type application/javascript;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
}
# Cache static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 7d;

28
offline.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MischLabs - Offline</title>
<meta name="theme-color" content="#0f172a">
<link rel="manifest" href="/manifest.webmanifest?v=4">
<link rel="shortcut icon" href="/favicon.ico?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32-v4.png">
<link rel="icon" type="image/png" sizes="48x48" href="/icons/favicon-48-v4.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/favicon-192-v4.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-v4.png">
<link rel="stylesheet" href="/style.css?v=30">
</head>
<body>
<main class="shell hero">
<div class="brand">
<img class="brand-mark" src="/icons/app-icon-source.png" alt="" width="72" height="72">
<div>
<p class="eyebrow">Offline</p>
<h1>MischLabs</h1>
<p class="subtitle">Die App ist installiert, aber die Dienste sind gerade nicht erreichbar.</p>
</div>
</div>
</main>
</body>
</html>

1385
server.js Normal file

File diff suppressed because it is too large Load Diff

129
services.json Normal file
View File

@@ -0,0 +1,129 @@
{
"categories": [
{
"id": "access",
"label": "Zugang",
"title": "Accounts & Sicherheit"
},
{
"id": "media",
"label": "Medien",
"title": "Bibliothek & Streaming"
},
{
"id": "work",
"label": "Arbeit",
"title": "Code, Projekte & Spiel"
},
{
"id": "personal",
"label": "Persoenlich",
"title": "Portfolio & private Projekte"
}
],
"services": [
{
"id": "sso",
"name": "Single Sign-On",
"description": "Account, Sitzungen und Sicherheit",
"domain": "auth.mischlabs.de",
"url": "https://auth.mischlabs.de/realms/mischlabs/account",
"category": "access",
"accent": "#f59e0b",
"icon": "shield",
"keywords": "single sign-on keycloak account login auth"
},
{
"id": "drive",
"name": "Drive",
"description": "Cloud-Speicher",
"domain": "drive.mischlabs.de",
"url": "https://drive.mischlabs.de",
"mobileUrl": "nextcloud://openFiles",
"category": "media",
"accent": "#60a5fa",
"icon": "folder",
"keywords": "drive nextcloud cloud speicher dateien"
},
{
"id": "passwords",
"name": "Passwords",
"description": "Passwort-Manager",
"domain": "password.mischlabs.de",
"url": "https://password.mischlabs.de",
"mobileUrl": "bitwarden://",
"category": "access",
"accent": "#eab308",
"icon": "lock",
"keywords": "passwords vaultwarden passwort manager security"
},
{
"id": "movies",
"name": "Movies",
"description": "Filme & Serien",
"domain": "movies.mischlabs.de",
"url": "https://movies.mischlabs.de",
"mobileUrl": "jellyfin://",
"category": "media",
"accent": "#a78bfa",
"icon": "film",
"keywords": "movies jellyfin filme serien media streaming"
},
{
"id": "audiobooks",
"name": "Audiobooks",
"description": "Hoerbuecher",
"domain": "audiobook.mischlabs.de",
"url": "https://audiobook.mischlabs.de",
"mobileUrl": "audiobookshelf://",
"category": "media",
"accent": "#22c55e",
"icon": "headphones",
"keywords": "audiobooks audiobookshelf hoerbuecher audio"
},
{
"id": "books",
"name": "Books",
"description": "Buecher & Bibliothek",
"domain": "books.mischlabs.de",
"url": "https://books.mischlabs.de",
"category": "media",
"accent": "#06b6d4",
"icon": "book",
"keywords": "books calibre calibre-web buecher bibliothek"
},
{
"id": "git",
"name": "Git",
"description": "Code-Repositories",
"domain": "git.mischlabs.de",
"url": "https://git.mischlabs.de",
"category": "work",
"accent": "#f97316",
"icon": "git",
"keywords": "git gitea code repositories ci actions"
},
{
"id": "michess",
"name": "MiChess",
"description": "Schach",
"domain": "michess.mischlabs.de",
"url": "https://michess.mischlabs.de",
"category": "personal",
"accent": "#10b981",
"icon": "chess",
"keywords": "michess schach game chess"
},
{
"id": "tom",
"name": "Tom",
"description": "Photography",
"domain": "tom.mischlabs.de",
"url": "https://tom.mischlabs.de",
"category": "personal",
"accent": "#ec4899",
"icon": "user",
"keywords": "tom photography mischkomposition portfolio persoenlich fotos"
}
]
}

2094
style.css

File diff suppressed because it is too large Load Diff

70
sw.js Normal file
View File

@@ -0,0 +1,70 @@
const CACHE_NAME = 'mischlabs-pwa-v27';
const APP_SHELL = [
'/',
'/index.html',
'/favorites.html',
'/offline.html',
'/style.css?v=34',
'/auth.js?v=2',
'/app.js?v=40',
'/favorites.js?v=1',
'/services.json?v=1',
'/manifest.webmanifest?v=4',
'/favicon.ico?v=4',
'/icons/favicon-32-v4.png',
'/icons/favicon-192-v4.png',
'/icons/favicon-512-v4.png',
'/icons/apple-touch-icon-v4.png',
'/icons/maskable-512-v4.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => (
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
))
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const request = event.request;
if (request.method !== 'GET') {
return;
}
// Do not intercept or cache API requests
const url = new URL(request.url);
if (url.pathname.startsWith('/api/')) {
return;
}
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(() => caches.match('/offline.html'))
);
return;
}
event.respondWith(
caches.match(request).then((cached) => (
cached || fetch(request).then((response) => {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseToCache));
return response;
})
))
);
});