diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb91515 --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# tiptoi-sync + +Web-App zum Suchen, Herunterladen und Synchronisieren von tiptoi-Büchern auf den tiptoi-Stift. Läuft als selbst-gehosteter Server (z. B. auf einem Raspberry Pi oder Rock Pi) und ist über den Browser erreichbar – auch vom Smartphone aus. + +![Scan](https://img.shields.io/badge/SolidJS-PWA-blue) +![Server](https://img.shields.io/badge/Bun-Elysia-orange) +![Docker](https://img.shields.io/badge/Docker-arm64-green) + +--- + +## Features + +- **Barcode-Scanner** – Buch per Kamera-Scan (QR/EAN) suchen +- **Freitextsuche** – nach Titel oder Artikelnummer suchen +- **Download** – GME-Datei + Cover automatisch von Ravensburger herunterladen +- **Bibliothek** – alle heruntergeladenen Bücher in der Übersicht +- **Stift-Sync** – tiptoi-Stift per USB anschließen, Bücher direkt aus der App auf den Stift kopieren +- **Stift-Status** – zeigt welche Bücher bereits auf dem Stift sind und welche fehlen +- **PWA** – kann auf dem Homescreen installiert werden (Android/iOS) + +--- + +## Architektur + +``` +Browser / PWA + │ HTTP + ▼ +┌─────────────────────────────────┐ +│ Bun + Elysia (Port 3000) │ +│ │ +│ /api/search → Ravensburger│ +│ /api/download → GME + Cover │ +│ /api/downloads → Bibliothek │ +│ /api/pen/detect → lsblk │ +│ /api/pen/files → Stift-Inhalt│ +│ /api/pen/sync → Dateien kop.│ +│ /api/pen/eject → umount │ +│ │ +│ /* → SolidJS SPA │ +└─────────────────────────────────┘ + │ │ + /data/ USB + (GME-Dateien) tiptoi-Stift +``` + +--- + +## Voraussetzungen + +- [Bun](https://bun.sh) ≥ 1.0 +- Node.js (nur für den Client-Build, via Bun verfügbar) +- Docker + Docker Compose (für Deployment) + +--- + +## Entwicklung (lokal) + +```bash +git clone +cd tiptoi-sync +bun install + +# Server + Client parallel starten +bun run dev +``` + +- Client: `http://localhost:5173` (Vite Dev Server mit HMR) +- Server: `http://localhost:3000` + +Der Vite Dev Server proxied `/api/*` automatisch zum Server. + +--- + +## Deployment + +### Docker (empfohlen) + +```bash +# Image bauen +docker compose build + +# Starten +docker compose up -d + +# Logs +docker compose logs -f +``` + +Die App ist danach unter `http://:3000` erreichbar. + +Heruntergeladene GME-Dateien werden im Verzeichnis `./data/` gespeichert (persistentes Volume). + +### Arm64 (Rock Pi, Raspberry Pi) – Cross-Build von x86 + +Auf der x86-Entwicklungsmaschine (mit **Podman**): + +```bash +# QEMU einmalig installieren +sudo apt install qemu-user-static # Debian/Ubuntu +sudo dnf install qemu-user-static # Fedora + +# Image bauen und direkt auf den Server laden +podman build --platform linux/arm64 -t tiptoi-sync:latest . \ + && podman save --format docker-archive tiptoi-sync:latest \ + | ssh user@rockpi 'docker load' + +# docker-compose.yml auf den Server kopieren und starten +scp docker-compose.yml user@rockpi:~/tiptoi-sync/ +ssh user@rockpi 'mkdir -p ~/tiptoi-sync/data && cd ~/tiptoi-sync && docker compose up -d' +``` + +--- + +## USB-Automount (Headless Debian/Armbian) + +Damit der tiptoi-Stift beim Einstecken automatisch gemountet wird (kein Desktop nötig): + +### 1. Mount-Scripts installieren + +```bash +sudo tee /usr/local/bin/usb-mount.sh > /dev/null << 'EOF' +#!/bin/bash +set -euo pipefail +DEV="$1" +DEVPATH="/dev/$DEV" +FSTYPE=$(blkid -o value -s TYPE "$DEVPATH" 2>/dev/null || true) +[ -z "$FSTYPE" ] && exit 0 +LABEL=$(blkid -o value -s LABEL "$DEVPATH" 2>/dev/null | tr ' /\\' '___' || true) +UUID=$(blkid -o value -s UUID "$DEVPATH" 2>/dev/null || echo "$DEV") +NAME="${LABEL:-$UUID}" +MOUNTPOINT="/media/$NAME" +mkdir -p "$MOUNTPOINT" +mount "$DEVPATH" "$MOUNTPOINT" +logger -t usb-mount "Gemountet: $DEVPATH → $MOUNTPOINT ($FSTYPE)" +EOF +sudo chmod +x /usr/local/bin/usb-mount.sh + +sudo tee /usr/local/bin/usb-umount.sh > /dev/null << 'EOF' +#!/bin/bash +set -euo pipefail +DEV="$1" +MOUNTPOINT=$(findmnt -n -o TARGET "/dev/$DEV" 2>/dev/null || true) +if [ -n "$MOUNTPOINT" ]; then + umount "$MOUNTPOINT" + rmdir "$MOUNTPOINT" 2>/dev/null || true + logger -t usb-mount "Ausgeworfen: /dev/$DEV ($MOUNTPOINT)" +fi +EOF +sudo chmod +x /usr/local/bin/usb-umount.sh +``` + +### 2. Systemd-Service-Template + +```bash +sudo tee /etc/systemd/system/usb-mount@.service > /dev/null << 'EOF' +[Unit] +Description=USB-Mount %i + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/bin/usb-mount.sh %i +ExecStop=/usr/local/bin/usb-umount.sh %i +EOF +``` + +### 3. udev-Regel + +```bash +sudo tee /etc/udev/rules.d/99-usb-mount.rules > /dev/null << 'EOF' +ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", ENV{ID_FS_TYPE}!="", \ + TAG+="systemd", ENV{SYSTEMD_WANTS}+="usb-mount@%k.service" + +ACTION=="remove", SUBSYSTEMS=="usb", SUBSYSTEM=="block", \ + TAG+="systemd", RUN+="/bin/systemctl stop --no-block usb-mount@%k.service" +EOF +``` + +### 4. Aktivieren + +```bash +sudo udevadm control --reload-rules +sudo systemctl daemon-reload +``` + +### Testen + +```bash +# Stift einstecken, dann: +journalctl -t usb-mount -f + +# Manuell testen (Gerätname aus lsblk) +sudo systemctl start usb-mount@sdb.service +``` + +--- + +## Umgebungsvariablen + +| Variable | Standard | Beschreibung | +|---|---|---| +| `DOWNLOAD_DIR` | `./downloads` | Verzeichnis für GME-Dateien und Cover | +| `STATIC_DIR` | *(leer)* | Pfad zum gebauten Client (`client/dist`). Wenn gesetzt, liefert der Server die SPA aus. | + +--- + +## Projektstruktur + +``` +tiptoi-sync/ +├── client/ # SolidJS PWA (Vite) +│ └── src/ +│ ├── pages/ +│ │ ├── ScanPage.tsx # Barcode-Scanner + Suche +│ │ └── DownloadsPage.tsx # Bibliothek + Stift-Sync +│ └── components/ +│ └── QrScanner.tsx +├── server/ # Bun + Elysia API +│ └── src/ +│ └── index.ts +├── Dockerfile +├── docker-compose.yml +└── 99-usb-mount.rules # udev-Regel (Referenz) +``` + +--- + +## Hinweise + +- Der **Stift muss vom Host-System gemountet** werden (udev-Automount, s. o.). Der Docker-Container greift über `rshared`-Volume-Mount darauf zu. +- **Android-Direktsync** (Stift per USB-OTG ans Handy) ist technisch nicht möglich – Browser haben keinen Zugriff auf USB-Mass-Storage-Geräte. +- Zum **sicheren Auswerfen** aus der Web-Oberfläche muss der Container mit `privileged: true` laufen (siehe `docker-compose.yml`).