# LaunchPad Ein moderner, minimalistischer Homelab-Launcher – inspiriert von Raycast, Spotlight, Arc und Linear. Tippe wenige Buchstaben, finde sofort den gewünschten Dienst, drücke Enter. Kein überladenes Dashboard. Keine Kacheln. Im Mittelpunkt steht eine extrem schnelle Suche. ## Quickstart (Docker) ```bash git clone LaunchPad cd LaunchPad cp .env.example .env # optional anpassen, siehe unten docker compose up -d --build ``` - Frontend: http://localhost:8080 (HTTPS: https://localhost:8443, siehe unten) - Backend / Health-API: http://localhost:3001/api/health ## HTTPS im lokalen Netz LaunchPad ist per HTTPS auf Port `8443` erreichbar (`https://:8443`), zusätzlich zu HTTP auf `8080` – kein erzwungener Redirect, kein externer Dienst beteiligt. Der Frontend-Container erzeugt beim ersten Start automatisch eine **eigene lokale Root-CA** (`LaunchPad Local CA`) und signiert damit ein Server- Zertifikat für die in `.env` hinterlegte `LAUNCHPAD_HOST` (IP oder Hostname). Beides liegt in einem Docker-Volume (`launchpad-certs`) und bleibt über Neustarts und `--build` hinweg stabil – die CA wird nur einmal erzeugt, nicht bei jedem Deploy neu. **Ohne weiteren Schritt** funktioniert HTTPS sofort, der Browser zeigt aber eine Warnung (unbekannte CA). Für eine echte, warnungsfreie Verbindung – und damit Chrome die PWA auch wirklich "installiert" statt nur eine Verknüpfung anzulegen – reicht **ein einziger, pro Gerät einmaliger Schritt**: die Root-CA importieren. Es gibt dafür keinen Weg drumherum, der nicht doch einen externen Dienst braucht (siehe Kasten unten) – aber so ist es so einfach wie möglich gehalten. ### Root-CA einmal pro Gerät importieren ```bash # .env: IP/Hostname eintragen, unter der du zugreifst LAUNCHPAD_HOST=192.168.1.49 docker compose up -d --force-recreate frontend ``` **Am Handy (Android):** `http://192.168.1.49:8080/ca.crt` im Browser öffnen (bewusst HTTP, dafür braucht's noch kein Vertrauen) → Android bietet direkt "Zertifikat installieren" an → als "VPN und App" oder "WLAN"-Zertifikat speichern (je nach Android-Version heißt die Kategorie leicht anders, Hauptsache es landet unter "Von Nutzer installierte Zertifikate"). Danach ggf. kurz Chrome neu starten. **Am PC (Windows):** dieselbe URL im Browser öffnen, `ca.crt` herunterladen, Datei doppelklicken → "Zertifikat installieren" → "Lokaler Computer" → "Alle Zertifikate in folgendem Speicher speichern" → "Vertrauenswürdige Stammzertifizierungsstellen" → Fertigstellen. Kein zusätzliches Tool nötig. **macOS:** `ca.crt` herunterladen, doppelklicken (öffnet Schlüsselbundverwaltung), dort öffnen, Vertrauenseinstellung auf "Immer vertrauen" setzen. Danach zeigt `https://192.168.1.49:8443` keine Warnung mehr, und die PWA-Installation sollte funktionieren. > **Warum es keinen Weg ganz ohne diesen Schritt gibt:** Damit ein Browser > einem Zertifikat *automatisch*, ohne jeden manuellen Trust-Schritt, vertraut, > muss es entweder von einer öffentlich vorinstallierten CA stammen (z. B. > Let's Encrypt) – das erfordert zwangsläufig einen externen Dienst und meist > einen öffentlich auflösbaren Domainnamen – oder eben eine lokale CA, die du > einmal manuell für vertrauenswürdig erklärst. Es gibt keine dritte Option. > Diese Lösung minimiert den Aufwand auf "eine Datei öffnen, einmal pro Gerät", > mehr geht ohne externe Dienste nicht. ### Zertifikat wechseln / eigenes einbinden Falls du lieber ein eigenes Zertifikat verwendest (z. B. von einer internen CA, die du schon hast): `fullchain.pem` + `privkey.pem` ins Volume `launchpad-certs` legen (z. B. `docker cp fullchain.pem launchpad-frontend:/etc/nginx/certs/` bei laufendem Container, oder das Volume direkt mounten), danach `docker compose restart frontend`. Liegt dabei keine `ca-cert.pem` im selben Verzeichnis, generiert der Entrypoint nichts automatisch nach. ## Quickstart (lokale Entwicklung) Voraussetzungen: Node.js ≥ 20, pnpm ≥ 9. ```bash pnpm install pnpm dev:backend # startet Fastify auf :3001 pnpm dev:frontend # startet Vite auf :5173 (proxyt /api zum Backend) ``` ## Projektstruktur ``` LaunchPad │ ├── apps │ ├── frontend React + Vite + TypeScript + TailwindCSS │ └── backend Fastify + TypeScript + Drizzle ORM + SQLite │ ├── packages │ ├── shared gemeinsame Typen (Device, Service) + Such-Ranking-Logik │ └── ui gemeinsame UI-Komponenten (SearchInput, StatusBadge) │ ├── docker zusätzliche Docker-Hilfsdateien ├── docs Projektdokumentation ├── pnpm-workspace.yaml ├── docker-compose.yml └── tsconfig.base.json ``` ## Stand dieses Commits Dieser erste Commit liefert ein lauffähiges Grundgerüst: - ✅ pnpm-Monorepo mit `apps/*` und `packages/*` - ✅ Fastify-Backend mit `/api/health`-Endpunkt - ✅ SQLite-Datenbank (better-sqlite3) inkl. Schema für `devices`, `services`, `categories` (Drizzle ORM), automatisch angelegt beim Start - ✅ React-Startseite mit Suchfeld, Dark-/Light-Mode und Live-Statusanzeige des Backends - ✅ Tastaturkürzel `/` und `Strg+K` zum Fokussieren der Suche - ✅ Docker-Compose-Setup: `docker compose up -d --build` startet Frontend + Backend - ✅ Persistentes Docker-Volume für die SQLite-Datenbank - ✅ REST-API für Geräte & Dienste (`/api/devices`, `/api/services`), Zod-validiert, mit Repository-Layer über Drizzle (siehe `apps/backend/src/db/repositories`) - ✅ Suche im Frontend gegen echte Backend-Daten (TanStack Query), inkl. Ranking-Logik aus `packages/shared` und Tastatur-Navigation (Pfeiltasten, Enter, Escape) - ✅ Kategorien-API (`/api/categories`), inkl. Umbenennen, Löschen (Dienste behalten ihre Zuordnung nicht, werden aber nicht gelöscht) und Bulk-Reorder für Drag & Drop - ✅ Favoriten-Toggle direkt in der Trefferliste (Stern anklicken) - ✅ Scanner-Engine + API (`POST /api/scan/devices/:id`, `POST /api/scan/fritzbox`): DNS-Kandidaten (hostname/.home/.local), Portscan (80, 443 + typische Ports), Titel-/Favicon-Auslesen, Softwareerkennung, FritzBox-Geräteliste per TR-064 (HTTP-Digest-Auth). Läuft ausschließlich manuell per API-Aufruf – nie automatisch. Benutzeränderungen an Diensten (Name, Kategorie, Favorit, Alias, Icon, Reihenfolge) bleiben bei erneuten Scans garantiert erhalten. - ✅ Adminbereich unter `/admin` (TanStack Router): Dashboard, Geräte (inkl. „Jetzt scannen"-Button), Dienste (Inline-Bearbeitung), Kategorien (natives Drag & Drop), Scanner (FritzBox-Trigger + Sammel-Scan), Logs (Scan-Historie), Einstellungen (Live-Systeminfo + Theme), Plugins (ehrlicher Hinweis auf zukünftigen Commit) - ✅ PWA: installierbar (Manifest + Icons für Android/iOS/Desktop), Service Worker mit App-Shell-Precaching, `/api/*` läuft offline über den letzten Cache-Stand (NetworkFirst, 3s-Timeout) - ✅ Plugin-System: echte, ladbare Plugins unter `apps/backend/plugins/*` (Bind-Mount, kein Rebuild nötig). Plugins können die Softwareerkennung des Scanners erweitern (inkl. eigenem Icon) und eigene Geräte-Importquellen bereitstellen. Zwei funktionierende Beispiel-Plugins liegen bei. Noch **nicht** enthalten: - shadcn/ui, React Hook Form (aktuell einfache kontrollierte Formulare) - Eigene Admin-Routen/Menüpunkte pro Plugin (aktuell gesammelt auf einer Seite) ### Umgebungsvariablen (.env) Alle Umgebungsvariablen sind in `.env.example` dokumentiert. Für die lokale Entwicklung und für Docker Compose: ```bash cp .env.example .env # .env anpassen (v. a. FRITZBOX_* für den FritzBox-Scan) ``` `.env` ist in `.gitignore` und wird nie committet. **Auf dem Server (`/opt/LaunchPad`) muss sie nach dem ersten `git pull` einmalig manuell angelegt werden**, da git-ignorierte Dateien nicht mitgepullt werden: ```bash cd /opt/LaunchPad cp .env.example .env nano .env # FRITZBOX_* eintragen docker compose up -d --build ``` Fehlt `.env` komplett, startet alles trotzdem mit den in `docker-compose.yml` hinterlegten Defaults – der FritzBox-Scan liefert dann kontrolliert `HTTP 400` statt abzustürzen. Für `pnpm dev:backend` (ohne Docker) wird `.env` automatisch über `dotenv` geladen (`apps/backend/src/env.ts`, wird als allererstes importiert). Siehe [`docs/ROADMAP.md`](./docs/ROADMAP.md) für die geplante Reihenfolge. ## API-Endpunkte (Stand Commit 2) ``` GET /api/health GET /api/devices Liste aller Geräte inkl. ihrer Dienste GET /api/devices/:id POST /api/devices PATCH /api/devices/:id DELETE /api/devices/:id (löscht zugehörige Dienste per Cascade) GET /api/services optional ?deviceId=&category=&favorite=true GET /api/services/:id POST /api/services erfordert existierende deviceId PATCH /api/services/reorder Body: [{ id, order }, ...] PATCH /api/services/:id DELETE /api/services/:id GET /api/bookmarks POST /api/bookmarks Titel/Favicon/Beschreibung werden automatisch geladen, falls nicht angegeben PATCH /api/bookmarks/reorder Body: [{ id, order }, ...] PATCH /api/bookmarks/:id DELETE /api/bookmarks/:id GET /api/categories POST /api/categories optional color (Hex, z. B. "#3b82f6") PATCH /api/categories/reorder Body: [{ id, order }, ...] PATCH /api/categories/:id Umbenennen (aktualisiert automatisch alle Dienste mit altem Namen) DELETE /api/categories/:id Dienste behalten ihre category nicht mehr (null), werden aber nicht gelöscht POST /api/scan/devices/:id Netzwerk-Scan für ein Gerät; Antwort enthält zusätzlich staleServices (nicht mehr gefundene Dienste), nameChanges (abweichender Name/ Kategorie ggü. gespeichertem Wert) und deviceNameSuggestion (per Reverse-DNS) - nichts davon wird automatisch übernommen, nur zur Bestätigung im Adminbereich zurückgegeben POST /api/scan/fritzbox Liest Geräteliste der FritzBox per TR-064; Antwort enthält found (rohe Einträge) und uniqueDevices (nach MAC/IP zusammengeführt, kann kleiner als found sein - kein Datenverlust), zusätzlich staleDevices und deviceChanges (abweichender Name/IP ggü. gespeichertem Wert, nichts davon wird automatisch übernommen) GET /api/logs optional ?limit= (Default 100, Max 500) GET /api/transfer/export ?format=csv|xlsx|json (Default csv). Geräte UND Dienste in einer Datei, Spalte "type" unterscheidet die Zeilen. POST /api/transfer/import Body: { format, content: base64 }. Legt nur neue Geräte/Dienste an, überschreibt/ dupliziert nie Bestehendes. GET /api/recent-visits optional ?limit= POST /api/recent-visits Body: { itemType: "service"|"bookmark", itemId } DELETE /api/recent-visits GET /api/settings PATCH /api/settings Body: { recentVisitsLimit } GET /api/read-later POST /api/read-later Titel/Favicon werden automatisch geladen PATCH /api/read-later/:id DELETE /api/read-later/:id POST /api/read-later/:id/promote verschiebt zu Lesezeichen (als Favorit) POST /api/reset Löscht ALLE Geräte + Dienste (Cascade). Erfordert Body { "confirm": true }, sonst 400. GET /api/plugins geladene Plugins mit Capabilities POST /api/plugins/:name/import löst importDevices() eines Plugins aus ``` ## Frontend-Routen ``` / Startseite: minimalistische Suche /admin -> redirect zu /admin/dashboard /admin/dashboard /admin/devices /admin/services /admin/bookmarks /admin/categories /admin/scanner /admin/plugins Hinweis: Plugin-System noch nicht gebaut /admin/settings /admin/logs ``` ## Als Browser-Suchmaschine einrichten LaunchPad lässt sich als eigene Suchmaschine mit Kürzel in Chrome, Edge und Firefox hinterlegen – danach reicht z. B. `lp proxmox` in der Adresszeile, um direkt beim passenden Dienst zu landen (ohne LaunchPad überhaupt erst zu öffnen). Das ist ein **einmaliger, manueller Schritt pro Browser/Gerät** – kein Browser lässt eine Website das automatisch für sich einrichten, aus gutem Grund (Sicherheit). **Chrome / Edge:** 1. Einstellungen → „Suchmaschine" → „Suchmaschinen verwalten" 2. „Website-Suchmaschine hinzufügen" 3. Suchmaschine: `LaunchPad`, Kürzel: `lp`, URL: `https://:8443/search?q=%s` **Firefox:** 1. Einmal `https://:8443/` öffnen (Firefox erkennt den hinterlegten OpenSearch-Eintrag automatisch) 2. Einstellungen → „Suche" → bei „LaunchPad" ein Schlagwort (z. B. `lp`) eintragen Danach: `lp ` in die Adresszeile, Enter – landet bei eindeutigem Treffer direkt beim Dienst, sonst auf der LaunchPad-Startseite mit bereits eingetragenem Suchbegriff. ## Deployment auf unterschiedlichen Geräten Läuft überall, wo Docker Compose verfügbar ist. Ein paar konkrete Wege: ### Generischer Linux-Server / LXC-Container (Proxmox, etc.) ```bash git clone LaunchPad cd LaunchPad cp .env.example .env docker compose up -d --build ``` ### Synology NAS mit Container Manager (Portainer-ähnliche Oberfläche, DSM 7.2+) 1. Repo per `git clone` auf die NAS holen (z. B. per SSH, oder mit File Station hochladen) – Zielordner z. B. `/volume1/docker/LaunchPad`. 2. In Container Manager → „Projekt" → „Erstellen" → als Pfad den geklonten Ordner wählen. Container Manager erkennt die `docker-compose.yml` automatisch und bietet an, sie als Projekt zu importieren. 3. `.env` vorher (per File Station oder SSH) aus `.env.example` anlegen und anpassen – Container Manager selbst bietet dafür keine Oberfläche. 4. Projekt starten. Ports `8080`/`8443` müssen frei sein (in DSM ggf. mit bereits belegten NAS-eigenen Ports abgleichen, notfalls in `docker-compose.yml` die linke Seite der Port-Zuordnung ändern, z. B. `18080:80`). ### Synology NAS mit Portainer (falls separat installiert) 1. Repo wie oben auf die NAS holen. 2. Portainer → „Stacks" → „Add stack" → „Repository" (Git-URL direkt eintragen) oder „Upload" (`docker-compose.yml` hochladen). 3. Unter „Environment variables" die Inhalte aus `.env.example` eintragen (Portainer braucht dafür keine separate `.env`-Datei, nimmt die Stack-Vars). 4. „Deploy the stack". ### DockHand / andere Docker-Compose-Oberflächen Jede Oberfläche, die eine vorhandene `docker-compose.yml` importieren oder auf einen Git-Ordner zeigen kann, funktioniert grundsätzlich gleich: Repo bereitstellen (lokal geklont oder per Git-Integration der jeweiligen Oberfläche), `.env` aus `.env.example` befüllen, Compose-Datei starten/deployen. Es gibt keine LaunchPad-spezifischen Besonderheiten außer den Standard-Docker-Compose-Grundlagen (Ports, Volumes, `.env`). ## Deployment auf dem Server (xlc-launchpad) ```bash cd /opt/LaunchPad git pull docker compose up -d --build ``` ## Branching Entwicklung erfolgt ausschließlich auf `dev`. `main` bleibt der stabile Branch.